Update MessagePack & binary serialization through Jackson

This commit is contained in:
kuroppoi 2022-01-13 02:52:11 +01:00
parent 9f13408c84
commit 83b0b24a30
66 changed files with 799 additions and 1287 deletions

View file

@ -10,8 +10,8 @@ repositories {
dependencies { dependencies {
implementation 'org.apache.logging.log4j:log4j-api:2.15.0' implementation 'org.apache.logging.log4j:log4j-api:2.15.0'
implementation 'org.apache.logging.log4j:log4j-core:2.15.0' implementation 'org.apache.logging.log4j:log4j-core:2.15.0'
implementation 'org.msgpack:msgpack:0.6.12' implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.4'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.1' implementation 'org.msgpack:jackson-dataformat-msgpack:0.9.0'
implementation 'org.yaml:snakeyaml:1.27' implementation 'org.yaml:snakeyaml:1.27'
implementation 'org.reflections:reflections:0.9.12' implementation 'org.reflections:reflections:0.9.12'
implementation 'io.netty:netty-all:4.1.58.Final' implementation 'io.netty:netty-all:4.1.58.Final'

View file

@ -9,7 +9,6 @@ import org.apache.logging.log4j.Logger;
import brainwine.gameserver.command.CommandManager; import brainwine.gameserver.command.CommandManager;
import brainwine.gameserver.entity.player.PlayerManager; import brainwine.gameserver.entity.player.PlayerManager;
import brainwine.gameserver.loot.LootManager; import brainwine.gameserver.loot.LootManager;
import brainwine.gameserver.msgpack.MessagePackHelper;
import brainwine.gameserver.prefab.PrefabManager; import brainwine.gameserver.prefab.PrefabManager;
import brainwine.gameserver.server.NetworkRegistry; import brainwine.gameserver.server.NetworkRegistry;
import brainwine.gameserver.server.Server; import brainwine.gameserver.server.Server;
@ -38,7 +37,6 @@ public class GameServer {
logger.info("Starting GameServer ..."); logger.info("Starting GameServer ...");
CommandManager.init(); CommandManager.init();
GameConfiguration.init(); GameConfiguration.init();
MessagePackHelper.init();
lootManager = new LootManager(); lootManager = new LootManager();
prefabManager = new PrefabManager(); prefabManager = new PrefabManager();
StaticZoneGenerator.init(); StaticZoneGenerator.init();

View file

@ -68,7 +68,7 @@ public class ExportCommand extends Command {
executor.notify("Exporting your prefab ...", ALERT); executor.notify("Exporting your prefab ...", ALERT);
try { try {
prefabManager.registerPrefab(name, prefab); prefabManager.addPrefab(name, prefab);
executor.notify(String.format("Your prefab '%s' was successfully exported!", name), ALERT); executor.notify(String.format("Your prefab '%s' was successfully exported!", name), ALERT);
} catch (Exception e) { } catch (Exception e) {
executor.notify(String.format("An error occured while exporting prefab '%s': %s", name, e.getMessage()), ALERT); executor.notify(String.format("An error occured while exporting prefab '%s': %s", name, e.getMessage()), ALERT);

View file

@ -13,7 +13,7 @@ public abstract class ConfigurableDialog implements DialogComponent {
private final Map<String, Object> config = new HashMap<>(); private final Map<String, Object> config = new HashMap<>();
public abstract void init(); public abstract void init();
public abstract void handleResponse(Player player, String[] input); public abstract void handleResponse(Player player, Object[] input);
protected void addSection(DialogSection section) { protected void addSection(DialogSection section) {
List<Map<String, Object>> sections = (List<Map<String, Object>>)config.getOrDefault("sections", new ArrayList<>()); List<Map<String, Object>> sections = (List<Map<String, Object>>)config.getOrDefault("sections", new ArrayList<>());

View file

@ -37,14 +37,15 @@ public class RegistrationDialog extends ConfigurableDialog {
} }
@Override @Override
public void handleResponse(Player player, String[] input) { public void handleResponse(Player player, Object[] input) {
if(input.length != 2) { if(input.length != 2) {
player.alert("Incorrect number of parameters."); player.alert("Incorrect number of parameters.");
return; return;
} }
String email = input[0]; // TODO toString() for now, dialog system will be worked anyway.
String password = input[1]; String email = input[0].toString();
String password = input[1].toString();
if(email.length() > maxEmailLength || !emailPattern.matcher(email).matches()) { if(email.length() > maxEmailLength || !emailPattern.matcher(email).matches()) {
player.alert("Please enter a valid e-mail address."); player.alert("Please enter a valid e-mail address.");

View file

@ -1,8 +1,5 @@
package brainwine.gameserver.entity; package brainwine.gameserver.entity;
import brainwine.gameserver.msgpack.RegisterEnum;
@RegisterEnum
public enum EntityStatus { public enum EntityStatus {
EXITING, EXITING,

View file

@ -1,9 +1,7 @@
package brainwine.gameserver.entity; package brainwine.gameserver.entity;
import brainwine.gameserver.msgpack.EnumValue; import com.fasterxml.jackson.annotation.JsonValue;
import brainwine.gameserver.msgpack.RegisterEnum;
@RegisterEnum
public enum EntityType { public enum EntityType {
PLAYER(0), PLAYER(0),
@ -17,7 +15,7 @@ public enum EntityType {
this.id = id; this.id = id;
} }
@EnumValue @JsonValue
public int getId() { public int getId() {
return id; return id;
} }

View file

@ -1,13 +1,11 @@
package brainwine.gameserver.entity; package brainwine.gameserver.entity;
import brainwine.gameserver.msgpack.DefaultEnumValue; import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
import brainwine.gameserver.msgpack.EnumValue; import com.fasterxml.jackson.annotation.JsonValue;
import brainwine.gameserver.msgpack.RegisterEnum;
@RegisterEnum
public enum FacingDirection { public enum FacingDirection {
@DefaultEnumValue @JsonEnumDefaultValue
WEST(-1), WEST(-1),
EAST(1); EAST(1);
@ -17,7 +15,7 @@ public enum FacingDirection {
this.id = id; this.id = id;
} }
@EnumValue @JsonValue
public int getId() { public int getId() {
return id; return id;
} }

View file

@ -1,9 +1,7 @@
package brainwine.gameserver.entity.player; package brainwine.gameserver.entity.player;
import brainwine.gameserver.msgpack.EnumValue; import com.fasterxml.jackson.annotation.JsonValue;
import brainwine.gameserver.msgpack.RegisterEnum;
@RegisterEnum
public enum ChatType { public enum ChatType {
CHAT("c"), CHAT("c"),
@ -17,7 +15,7 @@ public enum ChatType {
this.id = id; this.id = id;
} }
@EnumValue @JsonValue
public String getId() { public String getId() {
return id; return id;
} }

View file

@ -1,9 +1,7 @@
package brainwine.gameserver.entity.player; package brainwine.gameserver.entity.player;
import brainwine.gameserver.msgpack.EnumValue; import com.fasterxml.jackson.annotation.JsonValue;
import brainwine.gameserver.msgpack.RegisterEnum;
@RegisterEnum
public enum ContainerType { public enum ContainerType {
INVENTORY("i"), INVENTORY("i"),
@ -16,7 +14,7 @@ public enum ContainerType {
this.id = id; this.id = id;
} }
@EnumValue @JsonValue
public String getId() { public String getId() {
return id; return id;
} }

View file

@ -3,10 +3,6 @@ package brainwine.gameserver.entity.player;
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.JsonValue;
import brainwine.gameserver.msgpack.EnumValue;
import brainwine.gameserver.msgpack.RegisterEnum;
@RegisterEnum
public enum KarmaLevel { public enum KarmaLevel {
GODLY("Godly", 500), GODLY("Godly", 500),
@ -28,7 +24,6 @@ public enum KarmaLevel {
} }
@JsonValue @JsonValue
@EnumValue
public String getId() { public String getId() {
return id; return id;
} }

View file

@ -1,9 +1,7 @@
package brainwine.gameserver.entity.player; package brainwine.gameserver.entity.player;
import brainwine.gameserver.msgpack.EnumValue; import com.fasterxml.jackson.annotation.JsonValue;
import brainwine.gameserver.msgpack.RegisterEnum;
@RegisterEnum
public enum NotificationType { public enum NotificationType {
ALERT(1), ALERT(1),
@ -25,7 +23,7 @@ public enum NotificationType {
this.id = id; this.id = id;
} }
@EnumValue @JsonValue
public int getId() { public int getId() {
return id; return id;
} }

View file

@ -318,7 +318,7 @@ public class Player extends Entity implements CommandExecutor {
sendMessage(new DialogMessage(id, dialog)); sendMessage(new DialogMessage(id, dialog));
} }
public void handleDialogInput(int id, String[] input) { public void handleDialogInput(int id, Object[] input) {
ConfigurableDialog dialog = dialogs.remove(id); ConfigurableDialog dialog = dialogs.remove(id);
if(dialog == null) { if(dialog == null) {

View file

@ -1,9 +1,7 @@
package brainwine.gameserver.entity.player; package brainwine.gameserver.entity.player;
import brainwine.gameserver.msgpack.EnumValue; import com.fasterxml.jackson.annotation.JsonValue;
import brainwine.gameserver.msgpack.RegisterEnum;
@RegisterEnum
public enum Skill { public enum Skill {
AGILITY, AGILITY,
@ -19,7 +17,7 @@ public enum Skill {
STAMINA, STAMINA,
SURVIVAL; SURVIVAL;
@EnumValue @JsonValue
public String getId() { public String getId() {
return toString().toLowerCase(); return toString().toLowerCase();
} }

View file

@ -2,10 +2,6 @@ package brainwine.gameserver.item;
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
import brainwine.gameserver.msgpack.DefaultEnumValue;
import brainwine.gameserver.msgpack.RegisterEnum;
@RegisterEnum
public enum Layer { public enum Layer {
BASE, BASE,
@ -13,7 +9,6 @@ public enum Layer {
FRONT, FRONT,
LIQUID, LIQUID,
@DefaultEnumValue
@JsonEnumDefaultValue @JsonEnumDefaultValue
NONE; NONE;
} }

View file

@ -1,11 +1,8 @@
package brainwine.gameserver.item; package brainwine.gameserver.item;
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
import com.fasterxml.jackson.annotation.JsonValue;
import brainwine.gameserver.msgpack.EnumValue;
import brainwine.gameserver.msgpack.RegisterEnum;
@RegisterEnum
public enum LootGraphic { public enum LootGraphic {
LOOT, LOOT,
@ -15,7 +12,7 @@ public enum LootGraphic {
@JsonEnumDefaultValue @JsonEnumDefaultValue
NONE; NONE;
@EnumValue @JsonValue
public String getId() { public String getId() {
return toString().toLowerCase(); return toString().toLowerCase();
} }

View file

@ -1,10 +0,0 @@
package brainwine.gameserver.msgpack;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DefaultEnumValue {}

View file

@ -1,14 +0,0 @@
package brainwine.gameserver.msgpack;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Can be assigned to a field in an Enumeration.
* When packed, the value of the first field with this annotation will be used instead of the ordinal.
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EnumValue {}

View file

@ -1,107 +0,0 @@
package brainwine.gameserver.msgpack;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.zip.DataFormatException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.msgpack.MessagePack;
import org.msgpack.packer.BufferPacker;
import org.msgpack.unpacker.BufferUnpacker;
import brainwine.gameserver.item.Item;
import brainwine.gameserver.msgpack.models.AppearanceData;
import brainwine.gameserver.msgpack.models.BlockUseData;
import brainwine.gameserver.msgpack.models.DialogInputData;
import brainwine.gameserver.msgpack.templates.AppearanceDataTemplate;
import brainwine.gameserver.msgpack.templates.BlockArrayTemplate;
import brainwine.gameserver.msgpack.templates.BlockTemplate;
import brainwine.gameserver.msgpack.templates.BlockUseDataTemplate;
import brainwine.gameserver.msgpack.templates.ChunkTemplate;
import brainwine.gameserver.msgpack.templates.DialogInputDataTemplate;
import brainwine.gameserver.msgpack.templates.EnumTemplate;
import brainwine.gameserver.msgpack.templates.ItemTemplate;
import brainwine.gameserver.msgpack.templates.PrefabTemplate;
import brainwine.gameserver.prefab.Prefab;
import brainwine.gameserver.util.ReflectionsHelper;
import brainwine.gameserver.util.ZipUtils;
import brainwine.gameserver.zone.Block;
import brainwine.gameserver.zone.Chunk;
/**
* Static instance for the MsgPack library.
*/
public class MessagePackHelper {
private static final Logger logger = LogManager.getLogger();
private static final MessagePack messagePack = new MessagePack();
public static void init() {
registerTemplates();
}
private static void registerTemplates() {
logger.info("Registering MessagePack templates ...");
messagePack.register(Item.class, new ItemTemplate());
messagePack.register(Block.class, new BlockTemplate());
messagePack.register(Block[].class, new BlockArrayTemplate());
messagePack.register(Chunk.class, new ChunkTemplate());
messagePack.register(Prefab.class, new PrefabTemplate());
messagePack.register(BlockUseData.class, new BlockUseDataTemplate());
messagePack.register(DialogInputData.class, new DialogInputDataTemplate());
messagePack.register(AppearanceData.class, new AppearanceDataTemplate());
registerEnumTemplates();
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static void registerEnumTemplates() {
for(Class<?> clazz : ReflectionsHelper.getTypesAnnotatedWith(RegisterEnum.class)) {
messagePack.register(clazz, new EnumTemplate(clazz));
}
}
public static BufferUnpacker readFile(File file) throws IOException, DataFormatException {
byte[] bytes = Files.readAllBytes(file.toPath());
bytes = ZipUtils.inflateBytes(bytes);
return createBufferUnpacker(bytes);
}
public static BufferUnpacker readFiles(File... files) throws IOException, DataFormatException, IndexOutOfBoundsException {
byte[] buffer = new byte[Short.MAX_VALUE];
int index = 0;
for(File file : files) {
byte[] bytes = Files.readAllBytes(file.toPath());
bytes = ZipUtils.inflateBytes(bytes);
System.arraycopy(bytes, 0, buffer, index, bytes.length);
index += bytes.length;
}
byte[] bytes = new byte[index];
System.arraycopy(buffer, 0, bytes, 0, bytes.length);
return createBufferUnpacker(bytes);
}
public static void writeToFile(File file, Object... objects) throws IOException {
BufferPacker packer = createBufferPacker();
for(Object object : objects) {
packer.write(object);
}
byte[] bytes = packer.toByteArray();
bytes = ZipUtils.deflateBytes(bytes);
packer.close();
Files.write(file.toPath(), bytes);
}
public static BufferUnpacker createBufferUnpacker(byte[] bytes) {
return messagePack.createBufferUnpacker(bytes);
}
public static BufferPacker createBufferPacker() {
return messagePack.createBufferPacker();
}
}

View file

@ -1,13 +0,0 @@
package brainwine.gameserver.msgpack;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Enumerations with this annotation will automatically be registered with {@link EnumTemplate}
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RegisterEnum {}

View file

@ -1,15 +0,0 @@
package brainwine.gameserver.msgpack.models;
import java.util.HashMap;
/**
* Extremely lazy, yes...
* Lazy, but it does the trick.
*/
public class AppearanceData extends HashMap<String, Object> {
/**
*
*/
private static final long serialVersionUID = -688912102884421443L;
}

View file

@ -1,17 +0,0 @@
package brainwine.gameserver.msgpack.models;
/**
* Simple wrapper to avoid having to force our custom template on object arrays.
*/
public class BlockUseData {
private final Object[] data;
public BlockUseData(Object[] data) {
this.data = data;
}
public Object[] getData() {
return data;
}
}

View file

@ -1,57 +0,0 @@
package brainwine.gameserver.msgpack.models;
import brainwine.gameserver.server.requests.DialogRequest;
/**
* For {@link DialogRequest}
* TODO Figure out more about all this.
*/
public class DialogInputData {
private String dialogName;
private int dialogId;
private String[] inputData;
private String action;
public DialogInputData(String dialogName) {
this.dialogName = dialogName;
}
public DialogInputData(int dialogId, String[] inputData) {
this.dialogId = dialogId;
this.inputData = inputData;
}
public DialogInputData(int dialogId, String action) {
this.dialogId = dialogId;
this.action = action;
}
public boolean isType1() {
return dialogName != null;
}
public boolean isType2() {
return dialogId != 0 && inputData != null;
}
public boolean isType3() {
return dialogId != 0 && action != null;
}
public String getDialogName() {
return dialogName;
}
public int getDialogId() {
return dialogId;
}
public String[] getInputData() {
return inputData;
}
public String getAction() {
return action;
}
}

View file

@ -1,60 +0,0 @@
package brainwine.gameserver.msgpack.templates;
import java.io.IOException;
import org.msgpack.MessageTypeException;
import org.msgpack.packer.Packer;
import org.msgpack.template.AbstractTemplate;
import org.msgpack.type.ValueType;
import org.msgpack.unpacker.Unpacker;
import brainwine.gameserver.msgpack.models.AppearanceData;
public class AppearanceDataTemplate extends AbstractTemplate<AppearanceData> {
@Override
public void write(Packer packer, AppearanceData data, boolean required) throws IOException {
if(data == null) {
if(required) {
throw new MessageTypeException("Attempted to write null");
}
packer.writeNil();
return;
}
packer.write(data);
}
@Override
public AppearanceData read(Unpacker unpacker, AppearanceData to, boolean required) throws IOException {
if(!required && unpacker.trySkipNil()) {
return null;
}
if(unpacker.getNextType() != ValueType.MAP) {
throw new MessageTypeException("Invalid data type");
}
AppearanceData data = new AppearanceData();
int numEntries = unpacker.readMapBegin();
for(int i = 0; i < numEntries; i++) {
String key = unpacker.readString();
Object value = null;
if(unpacker.getNextType() == ValueType.RAW) {
value = unpacker.readString();
} else if(unpacker.getNextType() == ValueType.INTEGER) {
value = unpacker.readInt();
} else {
throw new MessageTypeException("Invalid data type");
}
data.put(key, value);
}
unpacker.readMapEnd();
return data;
}
}

View file

@ -1,49 +0,0 @@
package brainwine.gameserver.msgpack.templates;
import java.io.IOException;
import org.msgpack.MessageTypeException;
import org.msgpack.packer.Packer;
import org.msgpack.template.AbstractTemplate;
import org.msgpack.unpacker.Unpacker;
import brainwine.gameserver.zone.Block;
public class BlockArrayTemplate extends AbstractTemplate<Block[]> {
@Override
public void write(Packer packer, Block[] blocks, boolean required) throws IOException {
if(blocks == null) {
if(required) {
throw new MessageTypeException("Attempted to write null");
}
packer.writeNil();
return;
}
packer.writeArrayBegin(blocks.length * 3);
for(Block block : blocks) {
packer.write(block);
}
packer.writeArrayEnd();
}
@Override
public Block[] read(Unpacker unpacker, Block[] to, boolean required) throws IOException {
if(!required && unpacker.trySkipNil()) {
return null;
}
Block[] blocks = new Block[unpacker.readArrayBegin() / 3];
for(int i = 0; i < blocks.length; i++) {
blocks[i] = unpacker.read(Block.class);
}
unpacker.readArrayEnd();
return blocks;
}
}

View file

@ -1,41 +0,0 @@
package brainwine.gameserver.msgpack.templates;
import java.io.IOException;
import org.msgpack.MessageTypeException;
import org.msgpack.packer.Packer;
import org.msgpack.template.AbstractTemplate;
import org.msgpack.unpacker.Unpacker;
import brainwine.gameserver.zone.Block;
public class BlockTemplate extends AbstractTemplate<Block> {
@Override
public void write(Packer packer, Block block, boolean required) throws IOException {
if(block == null) {
if(required) {
throw new MessageTypeException("Attempted to write null");
}
packer.writeNil();
return;
}
packer.write(block.getBaseItem().getId() | (((block.getLiquidItem().getId() & 255) << 8) | ((block.getLiquidMod() & 31) << 16)));
packer.write(block.getBackItem().getId() | ((block.getBackMod() & 31) << 16));
packer.write(block.getFrontItem().getId() | ((block.getFrontMod() & 31) << 16));
}
@Override
public Block read(Unpacker unpacker, Block to, boolean required) throws IOException {
if(!required && unpacker.trySkipNil()) {
return null;
}
int base = unpacker.readInt();
int back = unpacker.readInt();
int front = unpacker.readInt();
return new Block(base & 15, back & 65535, back >> 16 & 31, front & 65535, front >> 16 & 31, base >> 8 & 255, base >> 16 & 31);
}
}

View file

@ -1,71 +0,0 @@
package brainwine.gameserver.msgpack.templates;
import java.io.IOException;
import org.msgpack.MessageTypeException;
import org.msgpack.packer.Packer;
import org.msgpack.template.AbstractTemplate;
import org.msgpack.type.ValueType;
import org.msgpack.unpacker.Unpacker;
import brainwine.gameserver.msgpack.models.BlockUseData;
public class BlockUseDataTemplate extends AbstractTemplate<BlockUseData> {
@Override
public void write(Packer packer, BlockUseData data, boolean required) throws IOException {
if(data == null) {
if(required) {
throw new MessageTypeException("Attempted to write null");
}
packer.writeNil();
return;
}
packer.write(data.getData());
}
@Override
public BlockUseData read(Unpacker unpacker, BlockUseData to, boolean required) throws IOException {
if(!required && unpacker.trySkipNil()) {
return null;
}
if(unpacker.getNextType() == ValueType.ARRAY) {
Object[] data = new Object[unpacker.readArrayBegin()];
for(int i = 0; i < data.length; i++) {
data[i] = readObject(unpacker);
}
unpacker.readArrayEnd();
return new BlockUseData(data);
} else if(unpacker.getNextType() == ValueType.MAP) {
Object[] data = new Object[unpacker.readMapBegin()];
for(int i = 0; i < data.length; i++) {
unpacker.readString(); // Key, ignore
data[i] = readObject(unpacker);
}
unpacker.readMapEnd();
return new BlockUseData(data);
}
throw new MessageTypeException("Invalid data type");
}
private Object readObject(Unpacker unpacker) throws IOException {
switch(unpacker.getNextType()) {
case RAW:
return unpacker.readString();
case INTEGER:
return unpacker.readInt();
case FLOAT:
return unpacker.readFloat();
default:
throw new MessageTypeException("Invalid data type");
}
}
}

View file

@ -1,57 +0,0 @@
package brainwine.gameserver.msgpack.templates;
import java.io.IOException;
import org.msgpack.MessageTypeException;
import org.msgpack.packer.Packer;
import org.msgpack.template.AbstractTemplate;
import org.msgpack.unpacker.Unpacker;
import brainwine.gameserver.zone.Block;
import brainwine.gameserver.zone.Chunk;
public class ChunkTemplate extends AbstractTemplate<Chunk> {
@Override
public void write(Packer packer, Chunk chunk, boolean required) throws IOException {
if(chunk == null) {
if(required) {
throw new MessageTypeException("Attempted to write null");
}
packer.writeNil();
return;
}
Block[] blocks = chunk.getBlocks();
packer.writeArrayBegin(5);
packer.write(chunk.getX());
packer.write(chunk.getY());
packer.write(chunk.getWidth());
packer.write(chunk.getHeight());
packer.write(blocks);
packer.writeArrayEnd();
}
@Override
public Chunk read(Unpacker unpacker, Chunk to, boolean required) throws IOException {
if(!required && unpacker.trySkipNil()) {
return null;
}
unpacker.readArrayBegin();
int x = unpacker.readInt();
int y = unpacker.readInt();
int width = unpacker.readInt();
int height = unpacker.readInt();
Block[] blocks = unpacker.read(Block[].class);
Chunk chunk = new Chunk(x, y, width, height);
for(int i = 0; i < blocks.length; i++) {
chunk.setBlock(i, blocks[i]);
}
unpacker.readArrayEnd();
return chunk;
}
}

View file

@ -1,67 +0,0 @@
package brainwine.gameserver.msgpack.templates;
import java.io.IOException;
import org.msgpack.MessageTypeException;
import org.msgpack.packer.Packer;
import org.msgpack.template.AbstractTemplate;
import org.msgpack.type.ValueType;
import org.msgpack.unpacker.Unpacker;
import brainwine.gameserver.msgpack.models.DialogInputData;
public class DialogInputDataTemplate extends AbstractTemplate<DialogInputData> {
@Override
public void write(Packer packer, DialogInputData data, boolean required) throws IOException {
if(data == null) {
if(required) {
throw new MessageTypeException("Attempted to write null");
}
packer.writeNil();
return;
}
if(data.isType1()) {
packer.write(data.getDialogName());
} else if(data.isType2()) {
packer.write(data.getDialogId());
packer.write(data.getInputData());
}
}
@Override
public DialogInputData read(Unpacker unpacker, DialogInputData to, boolean required) throws IOException {
if(!required && unpacker.trySkipNil()) {
return null;
}
if(unpacker.getNextType() == ValueType.RAW) {
String dialogName = unpacker.readString();
unpacker.readValue(); // TODO find out if this is the action, or just garbage data.
return new DialogInputData(dialogName);
} else if(unpacker.getNextType() == ValueType.INTEGER) {
int id = unpacker.readInt();
if(unpacker.getNextType() == ValueType.RAW) {
return new DialogInputData(id, unpacker.readString());
} else if(unpacker.getNextType() == ValueType.ARRAY) {
return new DialogInputData(id, unpacker.read(String[].class));
}
int numEntries = unpacker.readMapBegin();
String[] input = new String[numEntries];
for(int i = 0; i < numEntries; i++) {
unpacker.readString(); // Key, ignore
input[i] = unpacker.readString();
}
unpacker.readMapEnd();
return new DialogInputData(id, input);
}
throw new MessageTypeException("Invalid data type");
}
}

View file

@ -1,106 +0,0 @@
package brainwine.gameserver.msgpack.templates;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.msgpack.MessageTypeException;
import org.msgpack.packer.Packer;
import org.msgpack.template.AbstractTemplate;
import org.msgpack.type.ValueType;
import org.msgpack.unpacker.Unpacker;
import brainwine.gameserver.msgpack.DefaultEnumValue;
import brainwine.gameserver.msgpack.EnumValue;
@SuppressWarnings("unchecked")
public class EnumTemplate<T> extends AbstractTemplate<T> {
private final Map<T, Object> ids = new HashMap<>();
private final Map<Object, T> values = new HashMap<>();
private T defaultValue;
public EnumTemplate(Class<T> type) {
T[] entries = type.getEnumConstants();
for(Field field : type.getFields()) {
if(field.getType() == type && field.isAnnotationPresent(DefaultEnumValue.class)) {
try {
defaultValue = (T)field.get(type);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new MessageTypeException(e);
}
}
if(field.isAnnotationPresent(EnumValue.class)) {
try {
for(T entry : entries) {
Object id = field.get(entry);
ids.put(entry, id);
values.put(id, entry);
}
return;
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new MessageTypeException(e);
}
}
}
for(Method method : type.getMethods()) {
if(method.isAnnotationPresent(EnumValue.class)) {
try {
for(T entry : entries) {
Object id = method.invoke(entry);
ids.put(entry, id);
values.put(id, entry);
}
return;
} catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
throw new MessageTypeException(e);
}
}
}
for(int i = 0; i < entries.length; i++) {
ids.put(entries[i], i);
values.put(i, entries[i]);
}
}
@Override
public void write(Packer packer, T target, boolean required) throws IOException {
if(target == null) {
if(required) {
throw new MessageTypeException("Attempted to write null");
}
packer.writeNil();
return;
}
// Dangerous, might throw an NPE
packer.write(ids.get(target));
}
@Override
public T read(Unpacker unpacker, T to, boolean required) throws IOException {
if(!required && unpacker.trySkipNil()) {
return null;
}
ValueType next = unpacker.getNextType();
if(next == ValueType.INTEGER) {
return values.getOrDefault(unpacker.readInt(), defaultValue);
} else if(next == ValueType.RAW) {
return values.getOrDefault(unpacker.readString(), defaultValue);
}
throw new MessageTypeException("Unsupported enum id type");
}
}

View file

@ -1,37 +0,0 @@
package brainwine.gameserver.msgpack.templates;
import java.io.IOException;
import org.msgpack.MessageTypeException;
import org.msgpack.packer.Packer;
import org.msgpack.template.AbstractTemplate;
import org.msgpack.unpacker.Unpacker;
import brainwine.gameserver.item.Item;
import brainwine.gameserver.item.ItemRegistry;
public class ItemTemplate extends AbstractTemplate<Item> {
@Override
public void write(Packer packer, Item item, boolean required) throws IOException {
if(item == null) {
if(required) {
throw new MessageTypeException("Attempted to write null");
}
packer.writeNil();
return;
}
packer.write(item.getId());
}
@Override
public Item read(Unpacker unpacker, Item to, boolean required) throws IOException {
if(!required && unpacker.trySkipNil()) {
return null;
}
return ItemRegistry.getItem(unpacker.readInt());
}
}

View file

@ -1,50 +0,0 @@
package brainwine.gameserver.msgpack.templates;
import java.io.IOException;
import org.msgpack.MessageTypeException;
import org.msgpack.packer.Packer;
import org.msgpack.template.AbstractTemplate;
import org.msgpack.unpacker.Unpacker;
import brainwine.gameserver.prefab.Prefab;
import brainwine.gameserver.zone.Block;
public class PrefabTemplate extends AbstractTemplate<Prefab> {
@Override
public void write(Packer packer, Prefab prefab, boolean required) throws IOException {
if(prefab == null) {
if(required) {
throw new MessageTypeException("Attempted to write null");
}
packer.writeNil();
return;
}
packer.write(prefab.getWidth());
packer.write(prefab.getHeight());
packer.write(prefab.getBlocks());
}
@Override
public Prefab read(Unpacker unpacker, Prefab to, boolean required) throws IOException {
if(!required && unpacker.trySkipNil()) {
return null;
}
int width = unpacker.readInt();
int height = unpacker.readInt();
Block[] blocks = unpacker.read(Block[].class);
if(to != null) {
to.setWidth(width);
to.setHeight(height);
to.setBlocks(blocks);
return to;
}
return new Prefab(width, height, blocks);
}
}

View file

@ -4,60 +4,37 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import brainwine.gameserver.GameServer; import brainwine.gameserver.GameServer;
import brainwine.gameserver.item.Item; import brainwine.gameserver.item.Item;
import brainwine.gameserver.util.WeightedMap; import brainwine.gameserver.util.WeightedMap;
import brainwine.gameserver.zone.Block; import brainwine.gameserver.zone.Block;
@JsonIgnoreProperties(ignoreUnknown = true)
public class Prefab { public class Prefab {
@JsonProperty("dungeon")
private boolean dungeon; private boolean dungeon;
@JsonProperty("ruin")
private boolean ruin; private boolean ruin;
@JsonProperty("loot")
private boolean loot; private boolean loot;
@JsonProperty("decay")
private boolean decay; private boolean decay;
@JsonProperty("mirrorable")
private boolean mirrorable; private boolean mirrorable;
private int width;
@JsonProperty("replace") private int height;
private Block[] blocks;
private Map<Item, WeightedMap<Item>> replacements = new HashMap<>(); private Map<Item, WeightedMap<Item>> replacements = new HashMap<>();
@JsonProperty("corresponding_replace")
private Map<Item, CorrespondingReplacement> correspondingReplacements = new HashMap<>(); private Map<Item, CorrespondingReplacement> correspondingReplacements = new HashMap<>();
@JsonProperty("metadata")
private Map<Integer, Map<String, Object>> metadata = new HashMap<>(); private Map<Integer, Map<String, Object>> metadata = new HashMap<>();
@JsonIgnore protected Prefab(PrefabConfig config, PrefabBlockData blockData) {
private int width; this(blockData.getWidth(), blockData.getHeight(), blockData.getBlocks(), config.getMetadata());
dungeon = config.isDungeon();
@JsonIgnore ruin = config.isRuin();
private int height; loot = config.hasLoot();
decay = config.hasDecay();
@JsonIgnore mirrorable = config.isMirrorable();
private Block[] blocks; replacements = config.getReplacements();
correspondingReplacements = config.getCorrespondingReplacements();
@JsonCreator
private Prefab() {}
@JsonIgnore
public Prefab(int width, int height, Block[] blocks) {
this(width, height, blocks, new HashMap<>());
} }
@JsonIgnore
public Prefab(int width, int height, Block[] blocks, Map<Integer, Map<String, Object>> metadata) { public Prefab(int width, int height, Block[] blocks, Map<Integer, Map<String, Object>> metadata) {
this.width = width; this.width = width;
this.height = height; this.height = height;
@ -89,6 +66,18 @@ public class Prefab {
public boolean isMirrorable() { public boolean isMirrorable() {
return mirrorable; return mirrorable;
} }
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public Block[] getBlocks() {
return blocks;
}
public Map<String, Object> getMetadata(int index) { public Map<String, Object> getMetadata(int index) {
return metadata.get(index); return metadata.get(index);
@ -105,28 +94,4 @@ public class Prefab {
public Map<Item, CorrespondingReplacement> getCorrespondingReplacements() { public Map<Item, CorrespondingReplacement> getCorrespondingReplacements() {
return correspondingReplacements; return correspondingReplacements;
} }
public void setWidth(int width) {
this.width = width;
}
public int getWidth() {
return width;
}
public void setHeight(int height) {
this.height = height;
}
public int getHeight() {
return height;
}
public void setBlocks(Block[] blocks) {
this.blocks = blocks;
}
public Block[] getBlocks() {
return blocks;
}
} }

View file

@ -0,0 +1,38 @@
package brainwine.gameserver.prefab;
import java.beans.ConstructorProperties;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import brainwine.gameserver.zone.Block;
@JsonIgnoreProperties(ignoreUnknown = true)
public class PrefabBlockData {
private int width;
private int height;
private Block[] blocks;
protected PrefabBlockData(Prefab prefab) {
this(prefab.getWidth(), prefab.getHeight(), prefab.getBlocks());
}
@ConstructorProperties({"width", "height", "blocks"})
public PrefabBlockData(int width, int height, Block[] blocks) {
this.width = width;
this.height = height;
this.blocks = blocks;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public Block[] getBlocks() {
return blocks;
}
}

View file

@ -0,0 +1,85 @@
package brainwine.gameserver.prefab;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import brainwine.gameserver.item.Item;
import brainwine.gameserver.util.WeightedMap;
@JsonIgnoreProperties(ignoreUnknown = true)
public class PrefabConfig {
@JsonProperty("dungeon")
private boolean dungeon;
@JsonProperty("ruin")
private boolean ruin;
@JsonProperty("loot")
private boolean loot;
@JsonProperty("decay")
private boolean decay;
@JsonProperty("mirrorable")
private boolean mirrorable;
@JsonProperty("replace")
private Map<Item, WeightedMap<Item>> replacements = new HashMap<>();
@JsonProperty("corresponding_replace")
private Map<Item, CorrespondingReplacement> correspondingReplacements = new HashMap<>();
@JsonProperty("metadata")
private Map<Integer, Map<String, Object>> metadata = new HashMap<>();
@JsonCreator
private PrefabConfig() {}
protected PrefabConfig(Prefab prefab) {
dungeon = prefab.isDungeon();
ruin = prefab.isRuin();
loot = prefab.hasLoot();
decay = prefab.hasDecay();
mirrorable = prefab.isMirrorable();
replacements = prefab.getReplacements();
correspondingReplacements = prefab.getCorrespondingReplacements();
metadata = prefab.getMetadata();
}
public boolean isDungeon() {
return dungeon;
}
public boolean isRuin() {
return ruin;
}
public boolean hasLoot() {
return loot;
}
public boolean hasDecay() {
return decay;
}
public boolean isMirrorable() {
return mirrorable;
}
public Map<Integer, Map<String, Object>> getMetadata() {
return metadata;
}
public Map<Item, WeightedMap<Item>> getReplacements() {
return replacements;
}
public Map<Item, CorrespondingReplacement> getCorrespondingReplacements() {
return correspondingReplacements;
}
}

View file

@ -9,24 +9,32 @@ import java.util.Set;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.msgpack.unpacker.BufferUnpacker; import org.msgpack.core.MessagePack;
import org.msgpack.core.MessageUnpacker;
import org.msgpack.jackson.dataformat.MessagePackFactory;
import org.reflections.Reflections; import org.reflections.Reflections;
import org.reflections.scanners.ResourcesScanner; import org.reflections.scanners.ResourcesScanner;
import brainwine.gameserver.msgpack.MessagePackHelper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import brainwine.gameserver.serialization.BlockDeserializer;
import brainwine.gameserver.serialization.BlockSerializer;
import brainwine.gameserver.util.ZipUtils;
import brainwine.gameserver.zone.Block;
import brainwine.shared.JsonHelper; import brainwine.shared.JsonHelper;
public class PrefabManager { public class PrefabManager {
private static final Logger logger = LogManager.getLogger(); private static final Logger logger = LogManager.getLogger();
private static final ObjectMapper mapper = new ObjectMapper(new MessagePackFactory())
.registerModule(new SimpleModule()
.addDeserializer(Block.class, BlockDeserializer.INSTANCE)
.addSerializer(BlockSerializer.INSTANCE));
private final File dataDir = new File("prefabs"); private final File dataDir = new File("prefabs");
private final Map<String, Prefab> prefabs = new HashMap<>(); private final Map<String, Prefab> prefabs = new HashMap<>();
public PrefabManager() { public PrefabManager() {
loadPrefabs();
}
private void loadPrefabs() {
logger.info("Loading prefabs ..."); logger.info("Loading prefabs ...");
if(!dataDir.exists()) { if(!dataDir.exists()) {
@ -60,34 +68,49 @@ public class PrefabManager {
private void loadPrefab(File file) { private void loadPrefab(File file) {
String name = file.getName(); String name = file.getName();
File configFile = new File(file, "config.json"); File legacyBlocksFile = new File(file, "blocks.cmp");
File blocksFile = new File(file, "blocks.dat");
try { try {
Prefab prefab = JsonHelper.readValue(configFile, Prefab.class); PrefabBlockData blockData = null;
BufferUnpacker unpacker = MessagePackHelper.readFile(new File(file, "blocks.cmp"));
unpacker.read(prefab); if(legacyBlocksFile.exists() && !blocksFile.exists()) {
unpacker.close(); logger.info("Updating blocks file for prefab '{}' ...", name);
prefabs.put(name, prefab); MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(
ZipUtils.inflateBytes(Files.readAllBytes(legacyBlocksFile.toPath())));
int width = unpacker.unpackInt();
int height = unpacker.unpackInt();
Block[] blocks = new Block[unpacker.unpackArrayHeader() / 3];
for(int i = 0; i < blocks.length; i++) {
blocks[i] = new Block(unpacker.unpackInt(), unpacker.unpackInt(), unpacker.unpackInt());
}
blockData = new PrefabBlockData(width, height, blocks);
Files.write(blocksFile.toPath(), ZipUtils.deflateBytes(mapper.writeValueAsBytes(blockData)));
legacyBlocksFile.delete();
} else {
blockData = mapper.readValue(ZipUtils.inflateBytes(Files.readAllBytes(blocksFile.toPath())), PrefabBlockData.class);
}
PrefabConfig config = JsonHelper.readValue(new File(file, "config.json"), PrefabConfig.class);
prefabs.put(name, new Prefab(config, blockData));
} catch(Exception e) { } catch(Exception e) {
logger.error("Could not load prefab {}:", name, e); logger.error("Could not load prefab {}:", name, e);
} }
} }
public void registerPrefab(String name, Prefab structure) throws Exception { public void addPrefab(String name, Prefab prefab) throws Exception {
if(prefabs.containsKey(name)) { if(prefabs.containsKey(name)) {
logger.warn("Duplicate prefab name: {}", name); logger.warn("Duplicate prefab name: {}", name);
return; return;
} }
savePrefab(name, structure); File prefabDir = new File(dataDir, name);
prefabs.put(name, structure); prefabDir.mkdirs();
} JsonHelper.writeValue(new File(prefabDir, "config.json"), new PrefabConfig(prefab));
Files.write(new File(prefabDir, "blocks.dat").toPath(), ZipUtils.deflateBytes(mapper.writeValueAsBytes(new PrefabBlockData(prefab))));
private void savePrefab(String name, Prefab structure) throws Exception { prefabs.put(name, prefab);
File outputDir = new File(dataDir, name);
outputDir.mkdirs();
MessagePackHelper.writeToFile(new File(outputDir, "blocks.cmp"), structure);
JsonHelper.writeValue(new File(outputDir, "config.json"), structure);
} }
public Prefab getPrefab(String name) { public Prefab getPrefab(String name) {

View file

@ -0,0 +1,28 @@
package brainwine.gameserver.serialization;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import brainwine.gameserver.zone.Block;
public class BlockDeserializer extends StdDeserializer<Block> {
public static final BlockDeserializer INSTANCE = new BlockDeserializer();
private static final long serialVersionUID = 4595727432327616509L;
protected BlockDeserializer() {
super(Block.class);
}
@Override
public Block deserialize(JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException {
int base = parser.getIntValue();
int back = parser.nextIntValue(0);
int front = parser.nextIntValue(0);
return new Block(base, back, front);
}
}

View file

@ -0,0 +1,26 @@
package brainwine.gameserver.serialization;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import brainwine.gameserver.zone.Block;
public class BlockSerializer extends StdSerializer<Block> {
public static final BlockSerializer INSTANCE = new BlockSerializer();
private static final long serialVersionUID = 4060486562629926309L;
protected BlockSerializer() {
super(Block.class);
}
@Override
public void serialize(Block block, JsonGenerator generator, SerializerProvider provider) throws IOException {
generator.writeNumber(block.getBase());
generator.writeNumber(block.getBack());
generator.writeNumber(block.getFront());
}
}

View file

@ -0,0 +1,24 @@
package brainwine.gameserver.serialization;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import brainwine.gameserver.item.Item;
public class ItemCodeSerializer extends StdSerializer<Item> {
public static final ItemCodeSerializer INSTANCE = new ItemCodeSerializer();
private static final long serialVersionUID = 8938614385421916365L;
protected ItemCodeSerializer() {
super(Item.class);
}
@Override
public void serialize(Item item, JsonGenerator generator, SerializerProvider provider) throws IOException {
generator.writeNumber(item.getId());
}
}

View file

@ -0,0 +1,53 @@
package brainwine.gameserver.serialization;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import brainwine.gameserver.server.Message;
public class MessageSerializer extends StdSerializer<Message> {
public static final MessageSerializer INSTANCE = new MessageSerializer();
private static final long serialVersionUID = 2310652788158728087L;
protected MessageSerializer() {
super(Message.class);
}
@Override
public void serialize(Message message, JsonGenerator generator, SerializerProvider serializers) throws IOException {
Field[] fields = message.getClass().getFields();
try {
if(message.isPrepacked()) {
if(fields.length != 1 || !Collection.class.isAssignableFrom(fields[0].getType())) {
throw new IOException("Invalid prepacked message.");
}
generator.writeObject(fields[0].get(message));
} else {
List<Object> fieldValues = new ArrayList<>();
for(Field field : fields) {
fieldValues.add(field.get(message));
}
if(message.isCollection()) {
generator.writeObject(Arrays.asList(fieldValues));
} else {
generator.writeObject(fieldValues);
}
}
} catch(IllegalArgumentException | IllegalAccessException e) {
throw new IOException(e);
}
}
}

View file

@ -0,0 +1,30 @@
package brainwine.gameserver.serialization;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import brainwine.gameserver.zone.Chunk;
public class NetworkChunkSerializer extends StdSerializer<Chunk> {
public static final NetworkChunkSerializer INSTANCE = new NetworkChunkSerializer();
private static final long serialVersionUID = -1573014029866696503L;
protected NetworkChunkSerializer() {
super(Chunk.class);
}
@Override
public void serialize(Chunk chunk, JsonGenerator generator, SerializerProvider provider) throws IOException {
generator.writeStartArray();
generator.writeNumber(chunk.getX());
generator.writeNumber(chunk.getY());
generator.writeNumber(chunk.getWidth());
generator.writeNumber(chunk.getHeight());
generator.writeObject(chunk.getBlocks());
generator.writeEndArray();
}
}

View file

@ -0,0 +1,60 @@
package brainwine.gameserver.serialization;
import java.io.IOException;
import java.lang.reflect.Field;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import brainwine.gameserver.server.OptionalField;
import brainwine.gameserver.server.Request;
public class RequestDeserializer extends StdDeserializer<Request> {
private static final long serialVersionUID = 42527921659694141L;
public RequestDeserializer(Class<? extends Request> type) {
super(type);
}
@Override
public Request deserialize(JsonParser parser, DeserializationContext context) throws IOException, JsonProcessingException {
try {
Request request = (Request)_valueClass.newInstance();
Field[] fields = request.getClass().getFields();
if(parser.currentToken() != JsonToken.START_ARRAY) {
throw new IOException("Got invalid token, expected START_ARRAY");
}
for(Field field : fields) {
boolean required = field.getAnnotation(OptionalField.class) == null;
JsonToken token = parser.nextToken();
if(token == JsonToken.VALUE_NULL) {
if(required) {
throw new IOException("Value is null, but field is required!");
}
continue;
} else if(token == JsonToken.END_ARRAY) {
if(required) {
throw new IOException("Array is end, but field is required!");
}
break;
}
Object value = context.findRootValueDeserializer(context.constructType(field.getGenericType())).deserialize(parser, context);
field.set(request, value);
}
return request;
} catch (InstantiationException | IllegalAccessException e) {
throw new IOException(e);
}
}
}

View file

@ -0,0 +1,27 @@
package brainwine.gameserver.serialization;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import brainwine.gameserver.server.Request;
public class RequestDeserializerModifier extends BeanDeserializerModifier {
public static final RequestDeserializerModifier INSTANCE = new RequestDeserializerModifier();
private RequestDeserializerModifier() {}
@Override
@SuppressWarnings("unchecked")
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
Class<?> clazz = beanDesc.getBeanClass();
if(Request.class.isAssignableFrom(clazz)) {
return new RequestDeserializer((Class<? extends Request>)clazz);
}
return deserializer;
}
}

View file

@ -1,43 +1,10 @@
package brainwine.gameserver.server; package brainwine.gameserver.server;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import org.msgpack.packer.Packer;
/** /**
* Messages are outgoing packets to the client. * Messages are outgoing packets to the client.
*/ */
public abstract class Message { public abstract class Message {
public void pack(Packer packer) throws IOException, IllegalArgumentException, IllegalAccessException {
Field[] fields = getClass().getFields();
if(isPrepacked()) {
if(fields.length != 1 || !Collection.class.isAssignableFrom(fields[0].getType())) {
throw new IOException("Prepacked messages may only contain 1 field that must be a Collection.");
}
packer.write(fields[0].get(this));
} else {
List<Object> data = new ArrayList<>();
for(Field field : fields) {
data.add(field.get(this));
}
if(isCollection()) {
packer.write(Arrays.asList(data));
} else {
packer.write(data);
}
}
}
public boolean isJson() { public boolean isJson() {
return false; return false;
} }

View file

@ -140,12 +140,8 @@ public class NetworkRegistry {
requests.put(id, type); requests.put(id, type);
} }
public static Request instantiateRequest(int id) throws InstantiationException, IllegalAccessException { public static Class<? extends Request> getRequestClass(int id){
if(!requests.containsKey(id)) { return requests.get(id);
return null;
}
return requests.get(id).newInstance();
} }
public static void registerMessage(Class<? extends Message> type, int id) { public static void registerMessage(Class<? extends Message> type, int id) {

View file

@ -1,13 +1,6 @@
package brainwine.gameserver.server; package brainwine.gameserver.server;
import java.io.IOException;
import java.lang.reflect.Field;
import org.msgpack.type.ValueType;
import org.msgpack.unpacker.Unpacker;
import brainwine.gameserver.server.pipeline.Connection; import brainwine.gameserver.server.pipeline.Connection;
import brainwine.gameserver.server.requests.BlocksIgnoreRequest;
/** /**
* Requests are incoming packets from the client. * Requests are incoming packets from the client.
@ -15,30 +8,4 @@ import brainwine.gameserver.server.requests.BlocksIgnoreRequest;
public abstract class Request { public abstract class Request {
public abstract void process(Connection connection); public abstract void process(Connection connection);
/**
* Can be overriden for custom unpacking rules, as seen in {@link BlocksIgnoreRequest}
*/
public void unpack(Unpacker unpacker) throws IllegalArgumentException, IllegalAccessException, IOException {
unpacker.readArrayBegin();
Field[] fields = this.getClass().getFields();
for(Field field : fields) {
try {
if(unpacker.getNextType() == ValueType.NIL && field.getAnnotation(OptionalField.class) == null) {
throw new IOException("Value is nil, but field is required!");
}
} catch(Exception e) {
if(field.getAnnotation(OptionalField.class) == null) {
throw e;
}
continue;
}
field.set(this, unpacker.read(field.getType()));
}
unpacker.readArrayEnd(true);
}
} }

View file

@ -7,7 +7,22 @@ import java.util.concurrent.ThreadFactory;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.msgpack.core.MessagePack;
import org.msgpack.jackson.dataformat.MessagePackFactory;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import brainwine.gameserver.serialization.BlockSerializer;
import brainwine.gameserver.serialization.ItemCodeSerializer;
import brainwine.gameserver.serialization.MessageSerializer;
import brainwine.gameserver.serialization.NetworkChunkSerializer;
import brainwine.gameserver.serialization.RequestDeserializerModifier;
import brainwine.gameserver.server.pipeline.Connection; import brainwine.gameserver.server.pipeline.Connection;
import brainwine.gameserver.server.pipeline.MessageEncoder; import brainwine.gameserver.server.pipeline.MessageEncoder;
import brainwine.gameserver.server.pipeline.RequestDecoder; import brainwine.gameserver.server.pipeline.RequestDecoder;
@ -32,6 +47,20 @@ public class Server {
private static final Logger logger = LogManager.getLogger(); private static final Logger logger = LogManager.getLogger();
private static final ThreadFactory threadFactory = new DefaultThreadFactory("netty"); private static final ThreadFactory threadFactory = new DefaultThreadFactory("netty");
private static final ObjectMapper mapper = new ObjectMapper(new MessagePackFactory(
MessagePack.DEFAULT_PACKER_CONFIG.withStr8FormatSupport(false)))
.configure(SerializationFeature.WRITE_ENUMS_USING_INDEX, true)
.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE, true)
.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true)
.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true)
.registerModule(new SimpleModule()
.setDeserializerModifier(RequestDeserializerModifier.INSTANCE)
.addSerializer(MessageSerializer.INSTANCE)
.addSerializer(BlockSerializer.INSTANCE)
.addSerializer(NetworkChunkSerializer.INSTANCE)
.addSerializer(ItemCodeSerializer.INSTANCE));
private static final ObjectWriter writer = mapper.writer();
private static final ObjectReader reader = mapper.reader();
private final List<ChannelFuture> endpoints = new ArrayList<>(); private final List<ChannelFuture> endpoints = new ArrayList<>();
private final Class<? extends ServerChannel> channelType; private final Class<? extends ServerChannel> channelType;
private final EventLoopGroup eventLoopGroup; private final EventLoopGroup eventLoopGroup;
@ -55,8 +84,8 @@ public class Server {
protected void initChannel(Channel channel) throws Exception { protected void initChannel(Channel channel) throws Exception {
Connection connection = new Connection(); Connection connection = new Connection();
channel.pipeline().addLast("framer", new LengthFieldBasedFrameDecoder(ByteOrder.LITTLE_ENDIAN, 1024, 1, 4, 0, 0, true)); channel.pipeline().addLast("framer", new LengthFieldBasedFrameDecoder(ByteOrder.LITTLE_ENDIAN, 1024, 1, 4, 0, 0, true));
channel.pipeline().addLast("encoder", new MessageEncoder(connection)); channel.pipeline().addLast("encoder", new MessageEncoder(writer, connection));
channel.pipeline().addLast("decoder", new RequestDecoder()); channel.pipeline().addLast("decoder", new RequestDecoder(reader));
channel.pipeline().addLast("handler", connection); channel.pipeline().addLast("handler", connection);
} }
}).bind(port).syncUninterruptibly()); }).bind(port).syncUninterruptibly());

View file

@ -5,9 +5,8 @@ import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.msgpack.packer.BufferPacker; import com.fasterxml.jackson.databind.ObjectWriter;
import brainwine.gameserver.msgpack.MessagePackHelper;
import brainwine.gameserver.server.Message; import brainwine.gameserver.server.Message;
import brainwine.gameserver.server.NetworkRegistry; import brainwine.gameserver.server.NetworkRegistry;
import brainwine.gameserver.util.ZipUtils; import brainwine.gameserver.util.ZipUtils;
@ -18,9 +17,11 @@ import io.netty.handler.codec.MessageToByteEncoder;
public class MessageEncoder extends MessageToByteEncoder<Message> { public class MessageEncoder extends MessageToByteEncoder<Message> {
private final ObjectWriter writer;
private final Connection connection; private final Connection connection;
public MessageEncoder(Connection connection) { public MessageEncoder(ObjectWriter writer, Connection connection) {
this.writer = writer;
this.connection = connection; this.connection = connection;
} }
@ -45,10 +46,7 @@ public class MessageEncoder extends MessageToByteEncoder<Message> {
bytes = JsonHelper.writeValueAsBytes(data); bytes = JsonHelper.writeValueAsBytes(data);
} else { } else {
BufferPacker packer = MessagePackHelper.createBufferPacker(); bytes = writer.writeValueAsBytes(in);
in.pack(packer);
bytes = packer.toByteArray();
packer.close();
} }
if(in.isCompressed()) { if(in.isCompressed()) {

View file

@ -3,17 +3,22 @@ package brainwine.gameserver.server.pipeline;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
import org.msgpack.unpacker.Unpacker; import com.fasterxml.jackson.databind.ObjectReader;
import brainwine.gameserver.msgpack.MessagePackHelper;
import brainwine.gameserver.server.Request;
import brainwine.gameserver.server.NetworkRegistry; import brainwine.gameserver.server.NetworkRegistry;
import brainwine.gameserver.server.Request;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder; import io.netty.handler.codec.MessageToMessageDecoder;
public class RequestDecoder extends MessageToMessageDecoder<ByteBuf> { public class RequestDecoder extends MessageToMessageDecoder<ByteBuf> {
private final ObjectReader reader;
public RequestDecoder(ObjectReader reader) {
this.reader = reader;
}
@Override @Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> out) throws Exception { protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> out) throws Exception {
int id = buf.readByte() & 0xFF; int id = buf.readByte() & 0xFF;
@ -23,17 +28,15 @@ public class RequestDecoder extends MessageToMessageDecoder<ByteBuf> {
throw new IOException("Request exceeds max length of 1024 bytes"); throw new IOException("Request exceeds max length of 1024 bytes");
} }
Request request = NetworkRegistry.instantiateRequest(id); Class<? extends Request> type = NetworkRegistry.getRequestClass(id);
if(request == null) { if(type == null) {
throw new IOException("Client sent invalid request: " + id); throw new IOException("Client sent invalid request: " + id);
} }
byte[] bytes = new byte[length]; byte[] bytes = new byte[length];
buf.readBytes(bytes); buf.readBytes(bytes);
Unpacker unpacker = MessagePackHelper.createBufferUnpacker(bytes); Request request = reader.readValue(bytes, type);
request.unpack(unpacker);
unpacker.close();
out.add(request); out.add(request);
} }
} }

View file

@ -1,7 +1,5 @@
package brainwine.gameserver.server.requests; package brainwine.gameserver.server.requests;
import org.msgpack.type.Value;
import brainwine.gameserver.GameServer; import brainwine.gameserver.GameServer;
import brainwine.gameserver.server.OptionalField; import brainwine.gameserver.server.OptionalField;
import brainwine.gameserver.server.Request; import brainwine.gameserver.server.Request;
@ -14,7 +12,7 @@ public class AuthenticateRequest extends Request {
public String authToken; public String authToken;
@OptionalField @OptionalField
public Value details; public Object details;
@Override @Override
public void process(Connection connection) { public void process(Connection connection) {

View file

@ -14,7 +14,6 @@ import brainwine.gameserver.item.ItemUseType;
import brainwine.gameserver.item.Layer; import brainwine.gameserver.item.Layer;
import brainwine.gameserver.loot.Loot; import brainwine.gameserver.loot.Loot;
import brainwine.gameserver.loot.LootManager; import brainwine.gameserver.loot.LootManager;
import brainwine.gameserver.msgpack.models.BlockUseData;
import brainwine.gameserver.server.OptionalField; import brainwine.gameserver.server.OptionalField;
import brainwine.gameserver.server.PlayerRequest; import brainwine.gameserver.server.PlayerRequest;
import brainwine.gameserver.util.MapHelper; import brainwine.gameserver.util.MapHelper;
@ -30,7 +29,7 @@ public class BlockUseRequest extends PlayerRequest {
public Layer layer; public Layer layer;
@OptionalField @OptionalField
public BlockUseData data; public Object[] data;
@Override @Override
public void process(Player player) { public void process(Player player) {
@ -40,7 +39,10 @@ public class BlockUseRequest extends PlayerRequest {
return; return;
} }
Object[] data = this.data == null ? null : this.data.getData(); if(data != null && data.length == 1 && data[0] instanceof Map) {
data = ((Map<?, ?>)data[0]).values().toArray();
}
Block block = zone.getBlock(x, y); Block block = zone.getBlock(x, y);
MetaBlock metaBlock = zone.getMetaBlock(x, y); MetaBlock metaBlock = zone.getMetaBlock(x, y);
Item item = block.getItem(layer); Item item = block.getItem(layer);

View file

@ -1,10 +1,5 @@
package brainwine.gameserver.server.requests; package brainwine.gameserver.server.requests;
import java.io.IOException;
import org.msgpack.type.ValueType;
import org.msgpack.unpacker.Unpacker;
import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.entity.player.Player;
import brainwine.gameserver.server.PlayerRequest; import brainwine.gameserver.server.PlayerRequest;
@ -15,23 +10,6 @@ public class BlocksIgnoreRequest extends PlayerRequest {
public int[] chunkIndexes; public int[] chunkIndexes;
@Override
public void unpack(Unpacker unpacker) throws IOException {
int length = unpacker.readArrayBegin();
if(unpacker.getNextType() == ValueType.INTEGER) {
chunkIndexes = new int[length];
for(int i = 0; i < length; i++) {
chunkIndexes[i] = unpacker.readInt();
}
} else {
chunkIndexes = unpacker.read(int[].class);
}
unpacker.readArrayEnd();
}
@Override @Override
public void process(Player player) { public void process(Player player) {
/** /**

View file

@ -11,7 +11,6 @@ import brainwine.gameserver.entity.player.ColorSlot;
import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.entity.player.Player;
import brainwine.gameserver.item.Item; import brainwine.gameserver.item.Item;
import brainwine.gameserver.item.ItemRegistry; import brainwine.gameserver.item.ItemRegistry;
import brainwine.gameserver.msgpack.models.AppearanceData;
import brainwine.gameserver.server.PlayerRequest; import brainwine.gameserver.server.PlayerRequest;
import brainwine.gameserver.server.messages.DialogMessage; import brainwine.gameserver.server.messages.DialogMessage;
import brainwine.gameserver.util.MapHelper; import brainwine.gameserver.util.MapHelper;
@ -22,7 +21,7 @@ import brainwine.gameserver.util.MapHelper;
*/ */
public class ChangeAppearanceRequest extends PlayerRequest { public class ChangeAppearanceRequest extends PlayerRequest {
public AppearanceData data; public Map<String, Object> data;
@Override @Override
public void process(Player player) { public void process(Player player) {

View file

@ -1,19 +1,28 @@
package brainwine.gameserver.server.requests; package brainwine.gameserver.server.requests;
import java.util.Map;
import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.entity.player.Player;
import brainwine.gameserver.msgpack.models.DialogInputData;
import brainwine.gameserver.server.PlayerRequest; import brainwine.gameserver.server.PlayerRequest;
public class DialogRequest extends PlayerRequest { public class DialogRequest extends PlayerRequest {
public DialogInputData data; public Object id;
public Object[] input;
@Override @Override
public void process(Player player) { public void process(Player player) {
if(data.isType1()) { if(input.length == 1 && input[0] instanceof Map) {
input = ((Map<?, ?>)input[0]).values().toArray();
}
if(id instanceof String) {
player.alert("Sorry, this action is not implemented yet."); player.alert("Sorry, this action is not implemented yet.");
} else if(data.isType2()) { return;
player.handleDialogInput(data.getDialogId(), data.getInputData()); } else if(id instanceof Integer) {
if((int)id > 0) {
player.handleDialogInput((int)id, input);
}
} }
} }
} }

View file

@ -1,14 +1,12 @@
package brainwine.gameserver.server.requests; package brainwine.gameserver.server.requests;
import org.msgpack.type.Value;
import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.entity.player.Player;
import brainwine.gameserver.server.PlayerRequest; import brainwine.gameserver.server.PlayerRequest;
public class EventRequest extends PlayerRequest { public class EventRequest extends PlayerRequest {
public String key; public String key;
public Value value; public Object value;
@Override @Override
public void process(Player player) {} public void process(Player player) {}

View file

@ -1,7 +1,5 @@
package brainwine.gameserver.server.requests; package brainwine.gameserver.server.requests;
import org.msgpack.type.Value;
import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.entity.player.Player;
import brainwine.gameserver.item.Item; import brainwine.gameserver.item.Item;
import brainwine.gameserver.server.OptionalField; import brainwine.gameserver.server.OptionalField;
@ -19,7 +17,7 @@ public class InventoryUseRequest extends PlayerRequest {
public int status; // 0 = select, 1 = start, 2 = stop public int status; // 0 = select, 1 = start, 2 = stop
@OptionalField @OptionalField
public Value details; // array public Object details; // array
@Override @Override
public void process(Player player) { public void process(Player player) {

View file

@ -1,13 +1,11 @@
package brainwine.gameserver.server.requests; package brainwine.gameserver.server.requests;
import org.msgpack.type.Value;
import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.entity.player.Player;
import brainwine.gameserver.server.PlayerRequest; import brainwine.gameserver.server.PlayerRequest;
public class RespawnRequest extends PlayerRequest { public class RespawnRequest extends PlayerRequest {
public Value status; public Object status;
@Override @Override
public void process(Player player) { public void process(Player player) {

View file

@ -1,13 +1,11 @@
package brainwine.gameserver.server.requests; package brainwine.gameserver.server.requests;
import org.msgpack.type.Value;
import brainwine.gameserver.entity.player.Player; import brainwine.gameserver.entity.player.Player;
import brainwine.gameserver.server.PlayerRequest; import brainwine.gameserver.server.PlayerRequest;
public class StatusRequest extends PlayerRequest { public class StatusRequest extends PlayerRequest {
public Value status; public Object status;
@Override @Override
public void process(Player player) { public void process(Player player) {

View file

@ -4,10 +4,6 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.JsonValue;
import brainwine.gameserver.msgpack.EnumValue;
import brainwine.gameserver.msgpack.RegisterEnum;
@RegisterEnum
public enum Biome { public enum Biome {
@JsonEnumDefaultValue @JsonEnumDefaultValue
@ -20,7 +16,6 @@ public enum Biome {
SPACE; SPACE;
@JsonValue @JsonValue
@EnumValue
public String getId() { public String getId() {
return toString().toLowerCase(); return toString().toLowerCase();
} }

View file

@ -1,5 +1,9 @@
package brainwine.gameserver.zone; package brainwine.gameserver.zone;
import java.beans.ConstructorProperties;
import com.fasterxml.jackson.annotation.JsonIgnore;
/** /**
* Simple & convenient class to store block data. * Simple & convenient class to store block data.
* Outside of allowing zones to be chopped up unto chunks, it doesn't * Outside of allowing zones to be chopped up unto chunks, it doesn't
@ -14,12 +18,17 @@ public class Chunk {
private final Block[] blocks; private final Block[] blocks;
private boolean modified; private boolean modified;
public Chunk(int x, int y, int width, int height) { @ConstructorProperties({"x", "y", "width", "height", "blocks"})
public Chunk(int x, int y, int width, int height, Block[] blocks) {
this.x = x; this.x = x;
this.y = y; this.y = y;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.blocks = new Block[width * height]; this.blocks = blocks;
}
public Chunk(int x, int y, int width, int height) {
this(x, y, width, height, new Block[width * height]);
for(int i = 0; i < blocks.length; i++) { for(int i = 0; i < blocks.length; i++) {
blocks[i] = new Block(); blocks[i] = new Block();
@ -30,6 +39,7 @@ public class Chunk {
this.modified = modified; this.modified = modified;
} }
@JsonIgnore
public boolean isModified() { public boolean isModified() {
return modified; return modified;
} }
@ -50,16 +60,6 @@ public class Chunk {
return height; return height;
} }
public void setBlock(int x, int y, Block block) {
setBlock(getBlockIndex(x % width, y % height), block);
}
public void setBlock(int index, Block block) {
if(isIndexInBounds(index)) {
blocks[index] = block;
}
}
public Block getBlock(int x, int y) { public Block getBlock(int x, int y) {
return getBlock(getBlockIndex(x % width, y % height)); return getBlock(getBlockIndex(x % width, y % height));
} }

View file

@ -1,134 +1,104 @@
package brainwine.gameserver.zone; package brainwine.gameserver.zone;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.util.Arrays;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.msgpack.unpacker.Unpacker; import org.msgpack.core.MessagePack;
import org.msgpack.core.MessageUnpacker;
import org.msgpack.jackson.dataformat.MessagePackFactory;
import brainwine.gameserver.msgpack.MessagePackHelper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import brainwine.gameserver.serialization.BlockDeserializer;
import brainwine.gameserver.serialization.BlockSerializer;
import brainwine.gameserver.util.ZipUtils; import brainwine.gameserver.util.ZipUtils;
public class ChunkManager { public class ChunkManager {
private static final Logger logger = LogManager.getLogger(); private static final Logger logger = LogManager.getLogger();
private static final String headerString = "brainwine blocks file";
private static final int latestVersion = 1;
private static final int dataOffset = headerString.length() + 4;
private static final int allocSize = 2048; private static final int allocSize = 2048;
private static boolean conversionNotified; private static final ObjectMapper mapper = new ObjectMapper(new MessagePackFactory())
.registerModule(new SimpleModule()
.addDeserializer(Block.class, BlockDeserializer.INSTANCE)
.addSerializer(BlockSerializer.INSTANCE));
private final Zone zone; private final Zone zone;
private final File blocksFile;
private RandomAccessFile file; private RandomAccessFile file;
private int dataOffset;
public ChunkManager(Zone zone) { public ChunkManager(Zone zone) {
this.zone = zone; this.zone = zone;
blocksFile = new File(zone.getDirectory(), "blocks.dat");
File legacyBlocksFile = new File(zone.getDirectory(), "blocks");
try { if(!blocksFile.exists() && legacyBlocksFile.exists()) {
if(file == null) { logger.info("Updating blocks file for zone {} ...", zone.getDocumentId());
File blocksFile = new File(zone.getDirectory(), "blocks.dat"); DataInputStream inputStream = null;
File legacyBlocksFile = new File(zone.getDirectory(), "blocks"); DataOutputStream outputStream = null;
try {
inputStream = new DataInputStream(new FileInputStream(legacyBlocksFile));
outputStream = new DataOutputStream(new FileOutputStream(blocksFile));
int chunkCount = zone.getChunkCount();
if(!blocksFile.exists()) { for(int i = 0; i < chunkCount; i++) {
blocksFile.getParentFile().mkdirs(); short length = inputStream.readShort();
blocksFile.createNewFile(); byte[] chunkBytes = new byte[length];
} inputStream.read(chunkBytes);
inputStream.skipBytes(2048 - length - 2);
file = new RandomAccessFile(blocksFile, "rw"); chunkBytes = ZipUtils.inflateBytes(chunkBytes);
MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(chunkBytes);
if(file.length() == 0) { unpacker.unpackArrayHeader();
file.writeUTF(headerString); int x = unpacker.unpackInt();
file.writeInt(latestVersion); int y = unpacker.unpackInt();
int width = unpacker.unpackInt();
int height = unpacker.unpackInt();
Block[] blocks = new Block[unpacker.unpackArrayHeader() / 3];
if(legacyBlocksFile.exists()) { for(int j = 0; j < blocks.length; j++) {
if(!conversionNotified) { blocks[j] = new Block(unpacker.unpackInt(), unpacker.unpackInt(), unpacker.unpackInt());
logger.info("One or more block data files need to be converted. This might take a while ...");
conversionNotified = true;
}
convertLegacyBlocksFile(legacyBlocksFile);
}
} else {
if(!file.readUTF().equals(headerString)) {
throw new IOException("Invalid header string");
} }
unpacker.close();
byte[] bytes = ZipUtils.deflateBytes(mapper.writeValueAsBytes(new Chunk(x, y, width, height, blocks)));
outputStream.writeShort(bytes.length);
outputStream.write(bytes);
outputStream.write(new byte[allocSize - bytes.length - 2]);
} }
}
} catch(Exception e) { inputStream.close();
logger.error("ChunkManager construction for zone {} failed", zone.getDocumentId(), e); outputStream.close();
} } catch(Exception e) {
} logger.error("Could not update blocks file for zone {}", zone.getDocumentId(), e);
private void convertLegacyBlocksFile(File legacyBlocksFile) throws Exception {
byte[] bytes = Files.readAllBytes(legacyBlocksFile.toPath());
for(int i = 0; i < bytes.length; i += 2048) {
short length = (short)(((bytes[i] & 0xFF) << 8) + (bytes[i + 1] & 0xFF));
byte[] chunkBytes = ZipUtils.inflateBytes(Arrays.copyOfRange(bytes, i + 2, i + 2 + length));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
Unpacker unpacker = MessagePackHelper.createBufferUnpacker(chunkBytes);
unpacker.readArrayBegin();
int x = unpacker.readInt();
int y = unpacker.readInt();
int width = unpacker.readInt();
int height = unpacker.readInt();
dos.writeInt(x);
dos.writeInt(y);
dos.writeInt(width);
dos.writeInt(height);
unpacker.readArrayBegin();
for(int j = 0; j < width * height; j++) {
dos.writeInt(unpacker.readInt());
dos.writeInt(unpacker.readInt());
dos.writeInt(unpacker.readInt());
} }
unpacker.close(); legacyBlocksFile.delete();
byte[] updatedBytes = ZipUtils.deflateBytes(baos.toByteArray());
dos.close();
file.seek(dataOffset + zone.getChunkIndex(x, y) * allocSize);
file.writeShort(updatedBytes.length);
file.write(updatedBytes);
} }
} }
public Chunk loadChunk(int index) { public Chunk loadChunk(int index) {
Chunk chunk = null;
DataInputStream dis = null;
try { try {
if(file == null) {
file = new RandomAccessFile(blocksFile, "rw");
}
file.seek(dataOffset + index * allocSize); file.seek(dataOffset + index * allocSize);
byte[] bytes = new byte[file.readShort()]; byte[] bytes = new byte[file.readShort()];
file.read(bytes); file.read(bytes);
return mapper.readValue(ZipUtils.inflateBytes(bytes), Chunk.class);
dis = new DataInputStream(new ByteArrayInputStream(ZipUtils.inflateBytes(bytes)));
chunk = new Chunk(dis.readInt(), dis.readInt(), dis.readInt(), dis.readInt());
for(int i = 0; i < zone.getChunkWidth() * zone.getChunkHeight(); i++) {
chunk.setBlock(i, new Block(dis.readInt(), dis.readInt(), dis.readInt()));
}
} catch(Exception e) { } catch(Exception e) {
logger.error("Could not load chunk {} of zone {}", index, zone.getDocumentId(), e); logger.error("Could not load chunk {} of zone {}", index, zone.getDocumentId(), e);
} finally {
if(dis != null) {
try {
dis.close();
} catch (IOException e) {
logger.warn("Resource could not be closed", e);
}
}
} }
return chunk; return null;
} }
public void saveModifiedChunks() { public void saveModifiedChunks() {
@ -140,38 +110,25 @@ public class ChunkManager {
} }
public void saveChunk(Chunk chunk) { public void saveChunk(Chunk chunk) {
DataOutputStream dos = null;
int index = zone.getChunkIndex(chunk.getX(), chunk.getY()); int index = zone.getChunkIndex(chunk.getX(), chunk.getY());
try { try {
ByteArrayOutputStream baos = new ByteArrayOutputStream(allocSize); if(file == null) {
dos = new DataOutputStream(baos); file = new RandomAccessFile(blocksFile, "rw");
dos.writeInt(chunk.getX()); }
dos.writeInt(chunk.getY());
dos.writeInt(chunk.getWidth()); byte[] bytes = ZipUtils.deflateBytes(mapper.writeValueAsBytes(chunk));
dos.writeInt(chunk.getHeight());
if(bytes.length > allocSize) {
for(Block block : chunk.getBlocks()) { throw new IOException("WARNING: bigger than alloc size: " + bytes.length);
dos.writeInt(block.getBase());
dos.writeInt(block.getBack());
dos.writeInt(block.getFront());
} }
byte[] bytes = ZipUtils.deflateBytes(baos.toByteArray());
file.seek(dataOffset + index * allocSize); file.seek(dataOffset + index * allocSize);
file.writeShort(bytes.length); file.writeShort(bytes.length);
file.write(bytes); file.write(bytes);
chunk.setModified(false); chunk.setModified(false);
} catch(Exception e) { } catch (IOException e) {
logger.error("Could not save chunk %s of zone %s", index, zone.getDocumentId(), e); logger.error("Could not save chunk {} of zone {}", index, zone.getDocumentId(), e);
} finally {
if(dos != null) {
try {
dos.close();
} catch (IOException e) {
logger.warn("Resource could not be closed", e);
}
}
} }
} }
} }

View file

@ -1,24 +0,0 @@
package brainwine.gameserver.zone;
public enum SizePreset {
MINI(1000, 400),
NORMAL(2000, 800),
XL(4000, 1600);
private final int width;
private final int height;
private SizePreset(int width, int height) {
this.width = width;
this.height = height;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
}

View file

@ -1,6 +1,5 @@
package brainwine.gameserver.zone; package brainwine.gameserver.zone;
import java.beans.ConstructorProperties;
import java.io.File; import java.io.File;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
@ -19,12 +18,7 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.msgpack.unpacker.BufferUnpacker;
import com.fasterxml.jackson.annotation.JacksonInject;
import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIncludeProperties;
import com.fasterxml.jackson.annotation.JsonValue;
import brainwine.gameserver.GameServer; import brainwine.gameserver.GameServer;
import brainwine.gameserver.entity.Entity; import brainwine.gameserver.entity.Entity;
@ -38,7 +32,6 @@ import brainwine.gameserver.item.ItemUseType;
import brainwine.gameserver.item.Layer; import brainwine.gameserver.item.Layer;
import brainwine.gameserver.item.MetaType; import brainwine.gameserver.item.MetaType;
import brainwine.gameserver.item.ModType; import brainwine.gameserver.item.ModType;
import brainwine.gameserver.msgpack.MessagePackHelper;
import brainwine.gameserver.prefab.Prefab; import brainwine.gameserver.prefab.Prefab;
import brainwine.gameserver.server.Message; import brainwine.gameserver.server.Message;
import brainwine.gameserver.server.messages.BlockChangeMessage; import brainwine.gameserver.server.messages.BlockChangeMessage;
@ -52,9 +45,7 @@ import brainwine.gameserver.server.messages.ZoneExploredMessage;
import brainwine.gameserver.server.messages.ZoneStatusMessage; import brainwine.gameserver.server.messages.ZoneStatusMessage;
import brainwine.gameserver.util.MapHelper; import brainwine.gameserver.util.MapHelper;
import brainwine.gameserver.util.MathUtils; import brainwine.gameserver.util.MathUtils;
import brainwine.shared.JsonHelper;
@JsonIncludeProperties({"name", "biome", "width", "height"})
public class Zone { public class Zone {
public static final int DEFAULT_CHUNK_WIDTH = 20; public static final int DEFAULT_CHUNK_WIDTH = 20;
@ -64,8 +55,8 @@ public class Zone {
private final Biome biome; private final Biome biome;
private final int width; private final int width;
private final int height; private final int height;
private final int chunkWidth; private final int chunkWidth = DEFAULT_CHUNK_WIDTH;
private final int chunkHeight; private final int chunkHeight = DEFAULT_CHUNK_HEIGHT;
private final int numChunksWidth; private final int numChunksWidth;
private final int numChunksHeight; private final int numChunksHeight;
private final int[] surface; private final int[] surface;
@ -88,23 +79,19 @@ public class Zone {
private final Map<Integer, MetaBlock> globalMetaBlocks = new HashMap<>(); private final Map<Integer, MetaBlock> globalMetaBlocks = new HashMap<>();
private final Map<Integer, MetaBlock> fieldBlocks = new HashMap<>(); private final Map<Integer, MetaBlock> fieldBlocks = new HashMap<>();
public Zone(String documentId, String name, Biome biome, SizePreset sizePreset) { protected Zone(String documentId, ZoneConfig config, ZoneData data) {
this(documentId, name, biome, sizePreset.getWidth(), sizePreset.getHeight()); this(documentId, config.getName(), config.getBiome(), config.getWidth(), config.getHeight());
surface = data.getSurface();
sunlight = data.getSunlight();
chunksExplored = data.getChunksExplored();
} }
@ConstructorProperties({"documentId", "name", "biome", "width", "height"}) public Zone(String documentId, String name, Biome biome, int width, int height) {
public Zone(@JacksonInject("documentId") String documentId, String name, Biome biome, int width, int height) {
this(documentId, name, biome, width, height, DEFAULT_CHUNK_WIDTH, DEFAULT_CHUNK_HEIGHT);
}
private Zone(String documentId, String name, Biome biome, int width, int height, int chunkWidth, int chunkHeight) {
this.documentId = documentId; this.documentId = documentId;
this.name = name; this.name = name;
this.biome = biome; this.biome = biome;
this.width = width; this.width = width;
this.height = height; this.height = height;
this.chunkWidth = chunkWidth;
this.chunkHeight = chunkHeight;
numChunksWidth = width / chunkWidth; numChunksWidth = width / chunkWidth;
numChunksHeight = height / chunkHeight; numChunksHeight = height / chunkHeight;
surface = new int[width]; surface = new int[width];
@ -139,29 +126,14 @@ public class Zone {
} }
} }
} }
lastTick = now;
ticks++;
} }
public void load() throws Exception { public void saveModifiedChunks() {
pendingSunlight.clear();
File dataDir = new File("zones", documentId);
File shapeFile = new File(dataDir, "shape.cmp");
BufferUnpacker unpacker = MessagePackHelper.readFiles(shapeFile);
unpacker.read(surface);
unpacker.read(sunlight);
pendingSunlight.addAll(Arrays.asList(unpacker.read(Integer[].class)));
unpacker.read(chunksExplored);
setMetaBlocks(JsonHelper.readList(new File(dataDir, "metablocks.json"), MetaBlock.class));
indexDungeons();
}
public void save() throws Exception {
File dataDir = new File("zones", documentId);
dataDir.mkdirs();
JsonHelper.writeValue(new File(dataDir, "config.json"), this);
chunkManager.saveModifiedChunks(); chunkManager.saveModifiedChunks();
removeInactiveChunks(); removeInactiveChunks();
MessagePackHelper.writeToFile(new File(dataDir, "shape.cmp"), surface, sunlight, pendingSunlight, chunksExplored);
JsonHelper.writeValue(new File(dataDir, "metablocks.json"), metaBlocks.values());
} }
/** /**
@ -615,6 +587,8 @@ public class Zone {
indexMetaBlock(getBlockIndex(x, y), metaBlock); indexMetaBlock(getBlockIndex(x, y), metaBlock);
} }
} }
indexDungeons();
} }
private boolean ownsMetaBlock(MetaBlock metaBlock, Player player) { private boolean ownsMetaBlock(MetaBlock metaBlock, Player player) {
@ -914,16 +888,6 @@ public class Zone {
return numChunksWidth * numChunksHeight; return numChunksWidth * numChunksHeight;
} }
@JsonValue
protected Map<String, Object> getJsonConfig() {
Map<String, Object> config = new HashMap<>();
config.put("name", name);
config.put("biome", biome);
config.put("width", width);
config.put("height", height);
return config;
}
/** /**
* @return A {@link Map} containing all the data necessary for use in {@link ConfigurationMessage}. * @return A {@link Map} containing all the data necessary for use in {@link ConfigurationMessage}.
*/ */

View file

@ -0,0 +1,38 @@
package brainwine.gameserver.zone;
import java.beans.ConstructorProperties;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ZoneConfig {
private final String name;
private final Biome biome;
private final int width;
private final int height;
@ConstructorProperties({"name", "biome", "width", "height"})
public ZoneConfig(String name, Biome biome, int width, int height) {
this.name = name;
this.biome = biome;
this.width = width;
this.height = height;
}
public String getName() {
return name;
}
public Biome getBiome() {
return biome;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
}

View file

@ -0,0 +1,38 @@
package brainwine.gameserver.zone;
import java.beans.ConstructorProperties;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ZoneData {
private int[] surface;
private int[] sunlight;
private int[] pendingSunlight;
private boolean[] chunksExplored;
@ConstructorProperties({"surface", "sunlight", "pending_sunlight", "chunks_explored"})
public ZoneData(int[] surface, int[] sunlight, int[] pendingSunlight, boolean[] chunksExplored) {
this.surface = surface;
this.sunlight = sunlight;
this.pendingSunlight = pendingSunlight;
this.chunksExplored = chunksExplored;
}
public int[] getSurface() {
return surface;
}
public int[] getSunlight() {
return sunlight;
}
public int[] getPendingSunlight() {
return pendingSunlight;
}
public boolean[] getChunksExplored() {
return chunksExplored;
}
}

View file

@ -1,6 +1,7 @@
package brainwine.gameserver.zone; package brainwine.gameserver.zone;
import java.io.File; import java.io.File;
import java.nio.file.Files;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator; import java.util.Comparator;
@ -13,9 +14,13 @@ import java.util.function.Predicate;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.msgpack.core.MessagePack;
import org.msgpack.core.MessageUnpacker;
import org.msgpack.jackson.dataformat.MessagePackFactory;
import com.fasterxml.jackson.databind.InjectableValues; import com.fasterxml.jackson.databind.ObjectMapper;
import brainwine.gameserver.util.ZipUtils;
import brainwine.gameserver.zone.gen.AsyncZoneGenerator; import brainwine.gameserver.zone.gen.AsyncZoneGenerator;
import brainwine.gameserver.zone.gen.StaticZoneGenerator; import brainwine.gameserver.zone.gen.StaticZoneGenerator;
import brainwine.shared.JsonHelper; import brainwine.shared.JsonHelper;
@ -24,12 +29,31 @@ public class ZoneManager {
private static final Logger logger = LogManager.getLogger(); private static final Logger logger = LogManager.getLogger();
private final AsyncZoneGenerator asyncGenerator = new AsyncZoneGenerator(this); private final AsyncZoneGenerator asyncGenerator = new AsyncZoneGenerator(this);
private final ObjectMapper mapper = new ObjectMapper(new MessagePackFactory());
private final File dataDir = new File("zones"); private final File dataDir = new File("zones");
private Map<String, Zone> zones = new HashMap<>(); private Map<String, Zone> zones = new HashMap<>();
private Map<String, Zone> zonesByName = new HashMap<>(); private Map<String, Zone> zonesByName = new HashMap<>();
public ZoneManager() { public ZoneManager() {
loadZones(); logger.info("Loading zone data ...");
dataDir.mkdirs();
for(File file : dataDir.listFiles()) {
if(file.isDirectory()) {
loadZone(file);
}
}
if(zones.isEmpty()) {
logger.info("No zones were loaded. Generating default zone ...");
Zone zone = StaticZoneGenerator.generateZone(Biome.PLAIN, 2000, 800);
saveZone(zone);
putZone(zone);
} else {
logger.info("Successfully loaded {} zone(s)", zonesByName.size());
}
logger.info("Starting zone generator thread ...");
asyncGenerator.setDaemon(true); asyncGenerator.setDaemon(true);
asyncGenerator.start(); asyncGenerator.start();
} }
@ -40,55 +64,78 @@ public class ZoneManager {
} }
} }
private void loadZone(File file) {
String id = file.getName();
File dataFile = new File(file, "zone.dat");
File shapeFile = new File(file, "shape.cmp");
try {
ZoneData data = null;
if(shapeFile.exists() && !dataFile.exists()) {
MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(ZipUtils.inflateBytes(Files.readAllBytes(shapeFile.toPath())));
int[] surface = new int[unpacker.unpackArrayHeader()];
for(int i = 0; i < surface.length; i++) {
surface[i] = unpacker.unpackInt();
}
int[] sunlight = new int[unpacker.unpackArrayHeader()];
for(int i = 0; i < sunlight.length; i++) {
sunlight[i] = unpacker.unpackInt();
}
int[] pendingSunlight = new int[unpacker.unpackArrayHeader()];
for(int i = 0; i < pendingSunlight.length; i++) {
pendingSunlight[i] = unpacker.unpackInt();
}
boolean[] chunksExplored = new boolean[unpacker.unpackArrayHeader()];
for(int i = 0; i < pendingSunlight.length; i++) {
chunksExplored[i] = unpacker.unpackBoolean();
}
data = new ZoneData(surface, sunlight, pendingSunlight, chunksExplored);
mapper.writeValue(dataFile, data);
shapeFile.delete();
} else {
data = mapper.readValue(ZipUtils.inflateBytes(Files.readAllBytes(dataFile.toPath())), ZoneData.class);
}
ZoneConfig config = JsonHelper.readValue(new File(file, "config.json"), ZoneConfig.class);
Zone zone = new Zone(id, config, data);
zone.setMetaBlocks(JsonHelper.readList(new File(file, "metablocks.json"), MetaBlock.class));
putZone(zone);
} catch (Exception e) {
logger.error("Zone load failure. id: {}", id, e);
}
}
public void saveZones() { public void saveZones() {
for(Zone zone : zonesByName.values()) { for(Zone zone : getZones()) {
saveZone(zone); saveZone(zone);
} }
} }
public void saveZone(Zone zone) { public void saveZone(Zone zone) {
File file = zone.getDirectory();
file.mkdirs();
try { try {
zone.save(); zone.saveModifiedChunks();
ZoneConfig config = JsonHelper.readValue(zone, ZoneConfig.class);
ZoneData data = JsonHelper.readValue(zone, ZoneData.class);
JsonHelper.writeValue(new File(file, "metablocks.json"), zone.getMetaBlocks());
JsonHelper.writeValue(new File(file, "config.json"), config);
Files.write(new File(file, "zone.dat").toPath(), ZipUtils.deflateBytes(mapper.writeValueAsBytes(data)));
} catch(Exception e) { } catch(Exception e) {
logger.error("Zone save failure. id: {}", zone.getDocumentId(), e); logger.error("Zone save failure. id: {}", zone.getDocumentId(), e);
} }
} }
private void loadZones() {
logger.info("Loading zone data ...");
dataDir.mkdirs();
File[] files = dataDir.listFiles();
if(files.length == 0) {
logger.info("Generating default zone ...");
Zone zone = StaticZoneGenerator.generateZone(Biome.PLAIN, 2000, 800);
saveZone(zone);
putZone(zone);
return;
}
for(File file : files) {
if(file.isDirectory()) {
loadZone(file);
}
}
logger.info("Successfully loaded {} zone(s)", zonesByName.size());
}
private void loadZone(File file) {
String id = file.getName();
try {
File configFile = new File(file, "config.json");
Zone zone = JsonHelper.readValue(configFile, Zone.class, new InjectableValues.Std().addValue("documentId", id));
zone.load();
putZone(zone);
} catch (Exception e) {
logger.error("Zone load failure. id: {}", id, e);
}
}
private void putZone(Zone zone) { private void putZone(Zone zone) {
String id = zone.getDocumentId(); String id = zone.getDocumentId();
String name = zone.getName(); String name = zone.getName();