From ae719d62879ab1eb6f871608e4d809182d66f620 Mon Sep 17 00:00:00 2001 From: Ronny Jauch Date: Sun, 11 Aug 2019 16:52:34 +0200 Subject: [PATCH] options-to-save-worlds-to-redis patch --- ...-save-worlds-to-redis-instead-of-fil.patch | 1555 +++++++++++++++++ 1 file changed, 1555 insertions(+) create mode 100644 Spigot-Server-Patches/0444-Added-options-to-save-worlds-to-redis-instead-of-fil.patch diff --git a/Spigot-Server-Patches/0444-Added-options-to-save-worlds-to-redis-instead-of-fil.patch b/Spigot-Server-Patches/0444-Added-options-to-save-worlds-to-redis-instead-of-fil.patch new file mode 100644 index 000000000000..f141c641ec7a --- /dev/null +++ b/Spigot-Server-Patches/0444-Added-options-to-save-worlds-to-redis-instead-of-fil.patch @@ -0,0 +1,1555 @@ +From 22ffcafc9871c63aa3dd49b09ff18773436ae02e Mon Sep 17 00:00:00 2001 +From: Ronny Jauch +Date: Sun, 11 Aug 2019 16:48:54 +0200 +Subject: [PATCH] Added options to save worlds to redis instead of filesystem + + +diff --git a/pom.xml b/pom.xml +index 9cbd4880..6de4e161 100644 +--- a/pom.xml ++++ b/pom.xml +@@ -51,6 +51,11 @@ + 4.5.2 + runtime + ++ ++ org.redisson ++ redisson ++ 3.11.2 ++ + ++ ++ io.netty ++ com.destroystokyo.paper.libs.io.netty ++ + + jline + org.bukkit.craftbukkit.libs.jline +diff --git a/src/main/java/com/destroystokyo/paper/PaperConfig.java b/src/main/java/com/destroystokyo/paper/PaperConfig.java +index 9f240c35..0345f48b 100644 +--- a/src/main/java/com/destroystokyo/paper/PaperConfig.java ++++ b/src/main/java/com/destroystokyo/paper/PaperConfig.java +@@ -232,6 +232,20 @@ public class PaperConfig { + if (enableFileIOThreadSleep) Bukkit.getLogger().info("Enabled sleeping between chunk saves, beware of memory issues"); + } + ++ public static boolean enableRedisSaving; ++ private static void enableRedisSaving() { ++ enableRedisSaving = getBoolean("settings.use-redis-for-world-saving", false); ++ if (enableRedisSaving) { ++ Bukkit.getLogger().info("Enabled redis for saving worlds."); ++ Bukkit.getLogger().info("THIS FEATURE IS FULLY EXPERIMENTAL, BE AWARE OF BUGS."); ++ } ++ } ++ ++ public static String redisSavingPrefix; ++ private static void redisSavingPrefix() { ++ redisSavingPrefix = getString("settings.redis-world-saving-prefix", "paper"); ++ } ++ + public static boolean loadPermsBeforePlugins = true; + private static void loadPermsBeforePlugins() { + loadPermsBeforePlugins = getBoolean("settings.load-permissions-yml-before-plugins", true); +diff --git a/src/main/java/com/destroystokyo/paper/redis/RandomAccessFileRedis.java b/src/main/java/com/destroystokyo/paper/redis/RandomAccessFileRedis.java +new file mode 100644 +index 00000000..a45f35d1 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/redis/RandomAccessFileRedis.java +@@ -0,0 +1,197 @@ ++package com.destroystokyo.paper.redis; ++ ++import com.destroystokyo.paper.util.WorldFileUtils; ++import com.google.common.primitives.Ints; ++import net.minecraft.server.MinecraftServer; ++import org.redisson.Redisson; ++import org.redisson.client.codec.ByteArrayCodec; ++import org.redisson.client.protocol.RedisCommand; ++import org.redisson.client.protocol.RedisStrictCommand; ++import org.redisson.client.protocol.convertor.IntegerReplayConvertor; ++import org.redisson.client.protocol.convertor.LongReplayConvertor; ++import org.redisson.command.CommandExecutor; ++ ++import java.io.*; ++import java.nio.ByteBuffer; ++import java.nio.MappedByteBuffer; ++import java.nio.channels.FileChannel; ++import java.nio.channels.FileLock; ++import java.nio.channels.ReadableByteChannel; ++import java.nio.channels.WritableByteChannel; ++ ++public class RandomAccessFileRedis { ++ ++ private static final RedisCommand GETRANGE = new RedisCommand<>("getrange"); ++ private static final RedisStrictCommand STRLEN = new RedisStrictCommand<>("strlen", new LongReplayConvertor()); ++ private static final RedisCommand SETRANGE = new RedisCommand<>("setrange", new IntegerReplayConvertor()); ++ private static final ByteArrayCodec CODEC = ByteArrayCodec.INSTANCE; ++ ++ private final String key; ++ private final CommandExecutor commandExecutor; ++ private int position = 0; ++ private long length = 0; ++ ++ public RandomAccessFileRedis(File file, String mode) { ++ this.key = WorldFileUtils.getRedisKeyFromRegionFile(file); ++ System.out.println("key = " + key); ++ commandExecutor = ((Redisson) MinecraftServer.getServer().getRedissonClient()).getCommandExecutor(); ++ this.length = redisReadLength(); ++ } ++ ++ private Long redisReadLength() { ++ return commandExecutor.read(key, CODEC, STRLEN, key); ++ } ++ ++ public long length() { ++ return redisReadLength(); ++ } ++ ++ public void write(byte[] a) throws IOException { ++ commandExecutor.read(key, CODEC, SETRANGE, key, position, a); ++ position += a.length; ++ length += a.length; ++ } ++ ++ public void write(int i) throws IOException { ++ byte[] bytes = Ints.toByteArray(i); ++ write(bytes); ++ } ++ ++ public void seek(long l) throws IOException { ++ position = (int) l; ++ } ++ ++ public int readInt() throws IOException { ++ byte[] read = commandExecutor.read(key, CODEC, GETRANGE, key, position, position += 4); ++ int array = Ints.fromByteArray(read); ++ return array; ++ } ++ ++ public byte readByte() throws IOException { ++ byte[] read = commandExecutor.read(key, CODEC, GETRANGE, key, position, position += 1); ++ return read[0]; ++ } ++ ++ public void read(byte[] abyte) throws IOException { ++ byte[] read = commandExecutor.read(key, CODEC, GETRANGE, key, position, position += abyte.length); ++ System.arraycopy(read, 0, abyte, 0, abyte.length); ++ } ++ ++ public void write(byte[] abyte, int i, int j) throws IOException { ++ byte[] copy = new byte[j - i]; ++ System.arraycopy(abyte, i, copy, 0, j); ++ write(copy); ++ } ++ ++ public void close() throws IOException { ++ ++ } ++ ++ public FileDescriptor getFD() throws IOException { ++ return null; ++ } ++ ++ public void writeInt(int i) throws IOException { ++ byte[] bytes = Ints.toByteArray(i); ++ write(bytes); ++ } ++ ++ public void writeByte(int i) throws IOException { ++ byte[] a = new byte[]{(byte) i}; ++ write(a); ++ } ++ ++ public FileChannel getChannel() { ++ RandomAccessFileRedis rafr = this; ++ ++ return new FileChannel() { ++ @Override ++ public int read(ByteBuffer dst) throws IOException { ++ byte[] bytes = dst.array(); ++ rafr.read(bytes); ++ dst.put(bytes); ++ return bytes.length; ++ } ++ ++ @Override ++ public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { ++ return 0; ++ } ++ ++ @Override ++ public int write(ByteBuffer src) throws IOException { ++ return 0; ++ } ++ ++ @Override ++ public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { ++ return 0; ++ } ++ ++ @Override ++ public long position() throws IOException { ++ return 0; ++ } ++ ++ @Override ++ public FileChannel position(long newPosition) throws IOException { ++ return null; ++ } ++ ++ @Override ++ public long size() throws IOException { ++ return 0; ++ } ++ ++ @Override ++ public FileChannel truncate(long size) throws IOException { ++ return null; ++ } ++ ++ @Override ++ public void force(boolean metaData) throws IOException { ++ ++ } ++ ++ @Override ++ public long transferTo(long position, long count, WritableByteChannel target) throws IOException { ++ return 0; ++ } ++ ++ @Override ++ public long transferFrom(ReadableByteChannel src, long position, long count) throws IOException { ++ return 0; ++ } ++ ++ @Override ++ public int read(ByteBuffer dst, long position) throws IOException { ++ return 0; ++ } ++ ++ @Override ++ public int write(ByteBuffer src, long position) throws IOException { ++ return 0; ++ } ++ ++ @Override ++ public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException { ++ return null; ++ } ++ ++ @Override ++ public FileLock lock(long position, long size, boolean shared) throws IOException { ++ return null; ++ } ++ ++ @Override ++ public FileLock tryLock(long position, long size, boolean shared) throws IOException { ++ return null; ++ } ++ ++ @Override ++ protected void implCloseChannel() throws IOException { ++ ++ } ++ }; ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/redis/RandomAccessWriter.java b/src/main/java/com/destroystokyo/paper/redis/RandomAccessWriter.java +new file mode 100644 +index 00000000..a1983924 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/redis/RandomAccessWriter.java +@@ -0,0 +1,97 @@ ++package com.destroystokyo.paper.redis; ++ ++import java.io.*; ++import java.nio.channels.FileChannel; ++ ++public class RandomAccessWriter { ++ ++ private boolean useRedis; ++ ++ private RandomAccessFile raf; ++ private RandomAccessFileRedis rafr; ++ ++ public RandomAccessWriter(File file, String mode, boolean useRedis) throws FileNotFoundException { ++ if(useRedis) { ++ rafr = new RandomAccessFileRedis(file, mode); ++ } else { ++ raf = new RandomAccessFile(file, mode); ++ } ++ this.useRedis = useRedis; ++ } ++ ++ public long length() throws IOException { ++ return useRedis ? rafr.length() : raf.length(); ++ } ++ ++ public void write(byte[] a) throws IOException { ++ if (useRedis) { ++ rafr.write(a); ++ } else { ++ raf.write(a); ++ } ++ } ++ ++ public void write(int i) throws IOException { ++ if (useRedis) { ++ rafr.write(i); ++ } else { ++ raf.write(i); ++ } ++ } ++ ++ public void writeInt(int i) throws IOException { ++ if (useRedis) { ++ rafr.writeInt(i); ++ } else { ++ raf.writeInt(i); ++ } ++ } ++ ++ public void seek(long l) throws IOException { ++ if (useRedis) { ++ rafr.seek(l); ++ } else { ++ raf.seek(l); ++ } ++ } ++ ++ public FileChannel getChannel() { ++ return useRedis ? rafr.getChannel() : raf.getChannel(); ++ } ++ ++ public int readInt() throws IOException { ++ return useRedis ? rafr.readInt() : raf.readInt(); ++ } ++ ++ public byte readByte() throws IOException { ++ return useRedis ? rafr.readByte() : raf.readByte(); ++ } ++ ++ public void read(byte[] abyte) throws IOException { ++ if (useRedis) { ++ rafr.read(abyte); ++ } else { ++ raf.read(abyte); ++ } ++ } ++ ++ public void write(byte[] abyte, int i, int j) throws IOException { ++ if (useRedis) { ++ rafr.write(abyte, i, j); ++ } else { ++ raf.write(abyte, i, j); ++ } ++ } ++ ++ public void close() throws IOException { ++ if (useRedis) { ++ rafr.close(); ++ } else { ++ raf.close(); ++ } ++ } ++ ++ public FileDescriptor getFD() throws IOException { ++ return useRedis ? rafr.getFD() : raf.getFD(); ++ } ++} +diff --git a/src/main/java/com/destroystokyo/paper/util/WorldFileUtils.java b/src/main/java/com/destroystokyo/paper/util/WorldFileUtils.java +new file mode 100644 +index 00000000..c0430fa7 +--- /dev/null ++++ b/src/main/java/com/destroystokyo/paper/util/WorldFileUtils.java +@@ -0,0 +1,64 @@ ++package com.destroystokyo.paper.util; ++ ++import com.destroystokyo.paper.PaperConfig; ++import com.mojang.datafixers.DataFixTypes; ++import com.mojang.datafixers.DataFixer; ++import net.minecraft.server.*; ++import org.redisson.api.RBinaryStream; ++ ++import javax.annotation.Nullable; ++import java.io.File; ++import java.io.FileInputStream; ++import java.util.regex.Pattern; ++ ++public class WorldFileUtils { ++ ++ public static String getWorldNameFromRegionFile(File file) { ++ String path = file.getPath(); ++ while(new File(path).getName().contains(".")){ ++ path = path.substring(0, path.lastIndexOf("\\")); ++ } ++ ++ String[] remove = {"advancements", "data", "datapacks", "DIM1", "DIM-1", "playerdata", "region", "stats"}; ++ String pattern = Pattern.quote(System.getProperty("file.separator")); ++ String[] splittedPath = path.split(pattern); ++ ++ for (int i = 0; i sessionLock = this.redissonClient ++ .getBucket(PaperConfig.redisSavingPrefix + ++ ":worlds:" + ++ worldserver.getWorld().getName() + ++ ":session.lock"); ++ sessionLock.deleteAsync(); ++ } ++ // Paper end + worldserver.close(); + } + } +@@ -952,7 +993,7 @@ public abstract class MinecraftServer implements IAsyncTaskHandler, IMojangStati + this.m.b().a(agameprofile); + } + +- this.methodProfiler.enter("save"); ++ this.methodProfiler.enter("save"); + + serverAutoSave = (autosavePeriod > 0 && this.ticks % autosavePeriod == 0); // Paper + int playerSaveInterval = com.destroystokyo.paper.PaperConfig.playerAutoSaveRate; +@@ -1875,4 +1916,18 @@ public abstract class MinecraftServer implements IAsyncTaskHandler, IMojangStati + return SERVER; + } + // CraftBukkit end ++ ++ // Paper start ++ public WorldServer getWorldServer(String world) { ++ return this.worldServer.values().stream() ++ .filter(worldServer-> worldServer.getWorld().getName().equals(world)) ++ .distinct() ++ .findFirst() ++ .orElse(null); ++ } ++ ++ public boolean isRedisEnabled() { ++ return PaperConfig.enableRedisSaving; ++ } ++ // Paper end + } +diff --git a/src/main/java/net/minecraft/server/PlayerList.java b/src/main/java/net/minecraft/server/PlayerList.java +index 012238ac..ad048d75 100644 +--- a/src/main/java/net/minecraft/server/PlayerList.java ++++ b/src/main/java/net/minecraft/server/PlayerList.java +@@ -40,8 +40,14 @@ import org.bukkit.event.player.PlayerQuitEvent; + import org.bukkit.event.player.PlayerRespawnEvent; + import org.bukkit.event.player.PlayerTeleportEvent.TeleportCause; + import org.bukkit.util.Vector; +-import org.spigotmc.event.player.PlayerSpawnLocationEvent; + // CraftBukkit end ++// Paper start ++import org.redisson.RedissonKeys; ++import org.redisson.api.RBucket; ++import org.redisson.api.RedissonClient; ++import org.spigotmc.event.player.PlayerSpawnLocationEvent; ++import com.destroystokyo.paper.util.WorldFileUtils; ++// Paper end + + public abstract class PlayerList { + +@@ -1426,6 +1432,8 @@ public abstract class PlayerList { + } + + public ServerStatisticManager getStatisticManager(EntityPlayer entityhuman) { ++ RedissonClient client = MinecraftServer.getServer().getRedissonClient(); // Paper ++ + UUID uuid = entityhuman.getUniqueID(); + ServerStatisticManager serverstatisticmanager = uuid == null ? null : (ServerStatisticManager) entityhuman.getStatisticManager(); + // CraftBukkit end +@@ -1434,13 +1442,26 @@ public abstract class PlayerList { + File file = new File(this.server.getWorldServer(DimensionManager.OVERWORLD).getDataManager().getDirectory(), "stats"); + File file1 = new File(file, uuid + ".json"); + +- if (!file1.exists()) { +- File file2 = new File(file, entityhuman.getDisplayName().getString() + ".json"); ++ if (!MinecraftServer.getServer().isRedisEnabled()) { // Paper - decide if we need redis or filesystem ++ if (!file1.exists()) { ++ File file2 = new File(file, entityhuman.getDisplayName().getString() + ".json"); + +- if (file2.exists() && file2.isFile()) { +- file2.renameTo(file1); ++ if (file2.exists() && file2.isFile()) { ++ file2.renameTo(file1); ++ } + } +- } ++ } else { // Paper start ++ String key1 = WorldFileUtils.getRedisKeyFromRegionFile(file1); ++ RBucket bucket1 = client.getBucket(key1); ++ if(!bucket1.isExists()) { ++ File file2 = new File(file, entityhuman.getDisplayName().getString() + ".json"); ++ String key2 = WorldFileUtils.getRedisKeyFromRegionFile(file2); ++ RBucket bucket2 = client.getBucket(key2); ++ if (bucket2.isExists()) { ++ bucket2.rename(key1); ++ } ++ } ++ } // Paper end + + serverstatisticmanager = new ServerStatisticManager(this.server, file1); + // this.o.put(uuid, serverstatisticmanager); // CraftBukkit +diff --git a/src/main/java/net/minecraft/server/RegionFile.java b/src/main/java/net/minecraft/server/RegionFile.java +index c754e5b2..288ca7f2 100644 +--- a/src/main/java/net/minecraft/server/RegionFile.java ++++ b/src/main/java/net/minecraft/server/RegionFile.java +@@ -10,12 +10,15 @@ import java.io.DataInputStream; + import java.io.DataOutputStream; + import java.io.File; + import java.io.IOException; +-import java.io.RandomAccessFile; + import java.util.List; + import java.util.zip.DeflaterOutputStream; + import java.util.zip.GZIPInputStream; + import java.util.zip.InflaterInputStream; + import javax.annotation.Nullable; ++// Paper start ++import com.destroystokyo.paper.redis.RandomAccessWriter; ++import com.destroystokyo.paper.util.WorldFileUtils; ++// Paper end + + public class RegionFile { + +@@ -26,7 +29,7 @@ public class RegionFile { + // Spigot end + private static final byte[] a = new byte[4096]; + private final File b;private File getFile() { return b; } // Paper - OBFHELPER +- private RandomAccessFile c;private RandomAccessFile getDataFile() { return c; } // Paper - OBFHELPER ++ private RandomAccessWriter c;private RandomAccessWriter getDataFile() { return c; } // Paper - OBFHELPER + private final int[] d = new int[1024];private int[] offsets = d; // Paper - OBFHELPER + private final int[] e = new int[1024];private int[] timestamps = e; // Paper - OBFHELPER + private List f; private List getFreeSectors() { return this.f; } // Paper - OBFHELPER +@@ -34,6 +37,9 @@ public class RegionFile { + private long h; + + public RegionFile(File file) { ++ ++ boolean useRedis = MinecraftServer.getServer().isRedisEnabled(); // Paper ++ + this.b = file; + this.g = 0; + +@@ -42,7 +48,8 @@ public class RegionFile { + this.h = file.lastModified(); + } + +- this.c = new RandomAccessFile(file, "rw"); ++ this.c = new RandomAccessWriter(file, "rw", useRedis); // Paper - this.c = new RandomAccessFile(file, "rw"); ++ + if (this.c.length() < 8192L) { // Paper - headers should be 8192 + this.c.write(RegionFile.a); + this.c.write(RegionFile.a); +@@ -385,12 +392,15 @@ public class RegionFile { + try { + timestamps[j1] = 0; + offsets[j1] = 0; +- RandomAccessFile file = getDataFile(); +- file.seek(j1 * 4); +- file.writeInt(0); ++ ++ // Paper start ++ RandomAccessWriter randomAccessWriter = getDataFile(); ++ randomAccessWriter.seek(j1 * 4); ++ randomAccessWriter.writeInt(0); + // clear the timestamp +- file.seek(4096 + j1 * 4); +- file.writeInt(0); ++ randomAccessWriter.seek(4096 + j1 * 4); ++ randomAccessWriter.writeInt(0); ++ // Paper end + org.bukkit.Bukkit.getLogger().log(java.util.logging.Level.SEVERE, "Deleted corrupt chunk (" + debug + ") " + getFile().getAbsolutePath(), e); + } catch (IOException e) { + +diff --git a/src/main/java/net/minecraft/server/RegionFileCache.java b/src/main/java/net/minecraft/server/RegionFileCache.java +index 369aaa84..364d6471 100644 +--- a/src/main/java/net/minecraft/server/RegionFileCache.java ++++ b/src/main/java/net/minecraft/server/RegionFileCache.java +@@ -1,7 +1,7 @@ + package net.minecraft.server; + + import com.destroystokyo.paper.exception.ServerInternalException; +-import com.google.common.collect.Maps; ++ + import java.io.DataInputStream; + import java.io.DataOutputStream; + import java.io.File; +@@ -11,6 +11,7 @@ import java.util.Map; + import javax.annotation.Nullable; + import com.destroystokyo.paper.PaperConfig; // Paper + import java.util.LinkedHashMap; // Paper ++import com.destroystokyo.paper.util.WorldFileUtils; // Paper + + public class RegionFileCache { + +@@ -20,13 +21,16 @@ public class RegionFileCache { + public static synchronized RegionFile a(File file, int i, int j) { + File file1 = new File(file, "region"); + File file2 = new File(file1, "r." + (i >> 5) + "." + (j >> 5) + ".mca"); ++ String worldName = WorldFileUtils.getWorldNameFromRegionFile(file); // Paper + RegionFile regionfile = (RegionFile) RegionFileCache.cache.get(file2); + + if (regionfile != null) { + return regionfile; + } else { + if (!file1.exists()) { +- file1.mkdirs(); ++ if(!MinecraftServer.getServer().isRedisEnabled()) { // Paper - only write file if we are not using redis ++ file1.mkdirs(); ++ } + } + + if (RegionFileCache.cache.size() >= 256) { +diff --git a/src/main/java/net/minecraft/server/ServerStatisticManager.java b/src/main/java/net/minecraft/server/ServerStatisticManager.java +index d622983b..6d30db02 100644 +--- a/src/main/java/net/minecraft/server/ServerStatisticManager.java ++++ b/src/main/java/net/minecraft/server/ServerStatisticManager.java +@@ -1,5 +1,6 @@ + package net.minecraft.server; + ++import com.destroystokyo.paper.util.WorldFileUtils; + import com.google.common.collect.Maps; + import com.google.common.collect.Sets; + import com.google.gson.JsonElement; +@@ -25,15 +26,22 @@ import org.apache.commons.io.FileUtils; + import org.apache.logging.log4j.LogManager; + import org.apache.logging.log4j.Logger; + ++// Paper start ++import org.redisson.api.RBucket; ++import org.redisson.api.RedissonClient; ++// Paper end ++ + public class ServerStatisticManager extends StatisticManager { + + private static final Logger b = LogManager.getLogger(); + private final MinecraftServer c; + private final File d; + private final Set> e = Sets.newHashSet(); ++ private final RedissonClient redissonClient; // Paper + private int f = -300; + + public ServerStatisticManager(MinecraftServer minecraftserver, File file) { ++ redissonClient = minecraftserver.getRedissonClient(); // Paper + this.c = minecraftserver; + this.d = file; + // Spigot start +@@ -43,22 +51,41 @@ public class ServerStatisticManager extends StatisticManager { + this.a.put( wrapper, entry.getValue().intValue() ); + } + // Spigot end +- if (file.isFile()) { +- try { +- this.a(minecraftserver.az(), FileUtils.readFileToString(file)); +- } catch (IOException ioexception) { +- ServerStatisticManager.b.error("Couldn't read statistics file {}", file, ioexception); +- } catch (JsonParseException jsonparseexception) { +- ServerStatisticManager.b.error("Couldn't parse statistics file {}", file, jsonparseexception); ++ if (!MinecraftServer.getServer().isRedisEnabled()) { // Paper ++ if (file.isFile()) { ++ try { ++ this.a(minecraftserver.az(), FileUtils.readFileToString(file)); ++ } catch (IOException ioexception) { ++ ServerStatisticManager.b.error("Couldn't read statistics file {}", file, ioexception); ++ } catch (JsonParseException jsonparseexception) { ++ ServerStatisticManager.b.error("Couldn't parse statistics file {}", file, jsonparseexception); ++ } + } ++ } else { ++ // Paper start ++ String key = WorldFileUtils.getRedisKeyFromRegionFile(file); ++ RBucket stats = redissonClient.getBucket(key); ++ if (stats.isExists()) { ++ try { ++ this.a(minecraftserver.az(), stats.get()); ++ } catch (JsonParseException jsonparseexception) { ++ ServerStatisticManager.b.error("Couldn't parse statistics key {}", key, jsonparseexception); ++ } ++ } ++ // Paper end + } +- + } + + public void a() { + if ( org.spigotmc.SpigotConfig.disableStatSaving ) return; // Spigot + try { +- FileUtils.writeStringToFile(this.d, this.b()); ++ if (!MinecraftServer.getServer().isRedisEnabled()) { // Paper ++ FileUtils.writeStringToFile(this.d, this.b()); ++ } else { // Paper start ++ String key = WorldFileUtils.getRedisKeyFromRegionFile(this.d); ++ RBucket bucket = redissonClient.getBucket(key); ++ bucket.set(this.b()); ++ } // Paper end + } catch (IOException ioexception) { + ServerStatisticManager.b.error("Couldn't save stats", ioexception); + } +diff --git a/src/main/java/net/minecraft/server/WorldNBTStorage.java b/src/main/java/net/minecraft/server/WorldNBTStorage.java +index 9be0e994..f7d1bb25 100644 +--- a/src/main/java/net/minecraft/server/WorldNBTStorage.java ++++ b/src/main/java/net/minecraft/server/WorldNBTStorage.java +@@ -1,5 +1,6 @@ + package net.minecraft.server; + ++import com.destroystokyo.paper.util.WorldFileUtils; + import com.mojang.datafixers.DataFixTypes; + import com.mojang.datafixers.DataFixer; + import java.io.DataInputStream; +@@ -15,13 +16,23 @@ import org.apache.logging.log4j.LogManager; + import org.apache.logging.log4j.Logger; + + // CraftBukkit start ++import java.util.ArrayList; ++import java.util.List; + import java.util.UUID; ++import java.util.stream.Collectors; ++import java.util.stream.StreamSupport; ++ + import org.bukkit.craftbukkit.entity.CraftPlayer; ++import org.redisson.api.RBinaryStream; ++import org.redisson.api.RBucket; ++import org.redisson.api.RKeys; ++import org.redisson.api.RedissonClient; + // CraftBukkit end + + public class WorldNBTStorage implements IDataManager, IPlayerFileData { + + private static final Logger b = LogManager.getLogger(); ++ private final RedissonClient redissonClient; + private final File baseDir; + private final File playerDir; + private final long sessionId = SystemUtils.getMonotonicMillis(); +@@ -32,6 +43,8 @@ public class WorldNBTStorage implements IDataManager, IPlayerFileData { + + public WorldNBTStorage(File file, String s, @Nullable MinecraftServer minecraftserver, DataFixer datafixer) { + this.a = datafixer; ++ this.redissonClient = minecraftserver.getRedissonClient(); ++ + // Paper start + if (com.destroystokyo.paper.PaperConfig.useVersionedWorld) { + File origBaseDir = new File(file, s); +@@ -49,6 +62,7 @@ public class WorldNBTStorage implements IDataManager, IPlayerFileData { + String[] dirs = {"advancements", "data", "datapacks", "playerdata", "stats"}; + for (String dir : dirs) { + File origPlayerData = new File(origBaseDir, dir); ++ System.out.println("origPlayerData = " + origPlayerData.getPath()); + File targetPlayerData = new File(baseDir, dir); + if (origPlayerData.exists() && !targetPlayerData.exists()) { + if (!printedHeader) { +@@ -89,7 +103,9 @@ public class WorldNBTStorage implements IDataManager, IPlayerFileData { + this.playerDir = new File(this.baseDir, "playerdata"); + this.f = s; + if (minecraftserver != null) { +- this.playerDir.mkdirs(); ++ if(!minecraftserver.isRedisEnabled()) { ++ this.playerDir.mkdirs(); ++ } + this.g = new DefinedStructureManager(minecraftserver, this.baseDir, datafixer); + } else { + this.g = null; +@@ -101,7 +117,15 @@ public class WorldNBTStorage implements IDataManager, IPlayerFileData { + private void j() { + try { + File file = new File(this.baseDir, "session.lock"); +- DataOutputStream dataoutputstream = new DataOutputStream(new FileOutputStream(file)); ++ DataOutputStream dataoutputstream; ++ if (MinecraftServer.getServer().isRedisEnabled()) { ++ String key = WorldFileUtils.getRedisKeyFromRegionFile(file); ++ RBinaryStream binaryStream = MinecraftServer.getServer().getRedissonClient().getBinaryStream(key); ++ dataoutputstream = new DataOutputStream(binaryStream.getOutputStream()); ++ file.deleteOnExit(); ++ } else { ++ dataoutputstream = new DataOutputStream(new FileOutputStream(file)); ++ } + + try { + dataoutputstream.writeLong(this.sessionId); +@@ -111,7 +135,11 @@ public class WorldNBTStorage implements IDataManager, IPlayerFileData { + + } catch (IOException ioexception) { + ioexception.printStackTrace(); +- throw new RuntimeException("Failed to check session lock for world located at " + this.baseDir + ", aborting. Stop the server and delete the session.lock in this world to prevent further issues."); // Spigot ++ String baseDir = this.baseDir.getPath(); ++ if (MinecraftServer.getServer().isRedisEnabled()) { ++ baseDir = WorldFileUtils.getRedisKeyFromRegionFile(this.baseDir); ++ } ++ throw new RuntimeException("Failed to check session lock for world located at " + baseDir + ", aborting. Stop the server and delete the session.lock in this world to prevent further issues."); // Spigot + } + } + +@@ -121,12 +149,24 @@ public class WorldNBTStorage implements IDataManager, IPlayerFileData { + + public void checkSession() throws ExceptionWorldConflict { + try { ++ DataInputStream datainputstream; + File file = new File(this.baseDir, "session.lock"); +- DataInputStream datainputstream = new DataInputStream(new FileInputStream(file)); ++ ++ if (MinecraftServer.getServer().isRedisEnabled()) { ++ String key = WorldFileUtils.getRedisKeyFromRegionFile(file); ++ RBinaryStream binaryStream = MinecraftServer.getServer().getRedissonClient().getBinaryStream(key); ++ datainputstream = new DataInputStream(binaryStream.getInputStream()); ++ } else { ++ datainputstream = new DataInputStream(new FileInputStream(file)); ++ } + + try { + if (datainputstream.readLong() != this.sessionId) { +- throw new ExceptionWorldConflict("The save for world located at " + this.baseDir + " is being accessed from another location, aborting"); // Spigot ++ String baseDir = this.baseDir.getPath(); ++ if (MinecraftServer.getServer().isRedisEnabled()) { ++ baseDir = WorldFileUtils.getRedisKeyFromRegionFile(this.baseDir); ++ } ++ throw new ExceptionWorldConflict("The save for world located at " + baseDir + " is being accessed from another location, aborting"); // Spigot + } + } finally { + datainputstream.close(); +@@ -143,18 +183,29 @@ public class WorldNBTStorage implements IDataManager, IPlayerFileData { + + @Nullable + public WorldData getWorldData() { ++ + File file = new File(this.baseDir, "level.dat"); ++ if (!MinecraftServer.getServer().isRedisEnabled()) { + +- if (file.exists()) { +- WorldData worlddata = WorldLoader.a(file, this.a); ++ if (file.exists()) { ++ WorldData worlddata = WorldLoader.a(file, this.a); + +- if (worlddata != null) { +- return worlddata; ++ if (worlddata != null) { ++ return worlddata; ++ } + } ++ ++ file = new File(this.baseDir, "level.dat_old"); ++ return file.exists() ? WorldLoader.a(file, this.a) : null; ++ } else { ++ String key = WorldFileUtils.getRedisKeyFromRegionFile(file); ++ RBucket levelDat = MinecraftServer.getServer().getRedissonClient().getBucket(key); ++ if (levelDat.isExists()) { ++ return WorldFileUtils.loadWorldDataFromRedis(file, this.a); ++ } ++ return null; + } + +- file = new File(this.baseDir, "level.dat_old"); +- return file.exists() ? WorldLoader.a(file, this.a) : null; + } + + public void saveWorldData(WorldData worlddata, @Nullable NBTTagCompound nbttagcompound) { +@@ -167,20 +218,30 @@ public class WorldNBTStorage implements IDataManager, IPlayerFileData { + File file = new File(this.baseDir, "level.dat_new"); + File file1 = new File(this.baseDir, "level.dat_old"); + File file2 = new File(this.baseDir, "level.dat"); +- +- NBTCompressedStreamTools.a(nbttagcompound2, (OutputStream) (new FileOutputStream(file))); +- if (file1.exists()) { +- file1.delete(); ++ OutputStream dst; ++ if (MinecraftServer.getServer().isRedisEnabled()) { ++ String key = WorldFileUtils.getRedisKeyFromRegionFile(file2); ++ RBinaryStream binaryStream = MinecraftServer.getServer().getRedissonClient().getBinaryStream(key); ++ dst = binaryStream.getOutputStream(); ++ } else { ++ dst = (OutputStream) (new FileOutputStream(file)); + } + +- file2.renameTo(file1); +- if (file2.exists()) { +- file2.delete(); +- } ++ NBTCompressedStreamTools.a(nbttagcompound2, dst); ++ if (!MinecraftServer.getServer().isRedisEnabled()) { ++ if (file1.exists()) { ++ file1.delete(); ++ } + +- file.renameTo(file2); +- if (file.exists()) { +- file.delete(); ++ file2.renameTo(file1); ++ if (file2.exists()) { ++ file2.delete(); ++ } ++ ++ file.renameTo(file2); ++ if (file.exists()) { ++ file.delete(); ++ } + } + } catch (Exception exception) { + exception.printStackTrace(); +@@ -199,12 +260,31 @@ public class WorldNBTStorage implements IDataManager, IPlayerFileData { + File file = new File(this.playerDir, entityhuman.bu() + ".dat.tmp"); + File file1 = new File(this.playerDir, entityhuman.bu() + ".dat"); + +- NBTCompressedStreamTools.a(nbttagcompound, (OutputStream) (new FileOutputStream(file))); +- if (file1.exists()) { +- file1.delete(); ++ System.out.println("file1.getPath() = " + file1.getPath()); ++ ++ OutputStream dst; ++ ++ String key = WorldFileUtils.getRedisKeyFromRegionFile(file); ++ String key1 = WorldFileUtils.getRedisKeyFromRegionFile(file1); ++ System.out.println("key = " + key); ++ System.out.println("key1 = " + key1); ++ if (MinecraftServer.getServer().isRedisEnabled()) { ++ RBinaryStream binaryStream = this.redissonClient.getBinaryStream(key); ++ dst = binaryStream.getOutputStream(); ++ } else { ++ dst = (new FileOutputStream(file)); + } ++ NBTCompressedStreamTools.a(nbttagcompound, dst); + +- file.renameTo(file1); ++ if (!MinecraftServer.getServer().isRedisEnabled()) { ++ if (file1.exists()) { ++ file1.delete(); ++ } ++ ++ file.renameTo(file1); ++ } else { ++ this.redissonClient.getBucket(key).rename(key1); ++ } + } catch (Exception exception) { + WorldNBTStorage.b.error("Failed to save player data for {}", entityhuman.getName(), exception); // Paper + } +@@ -215,32 +295,66 @@ public class WorldNBTStorage implements IDataManager, IPlayerFileData { + public NBTTagCompound load(EntityHuman entityhuman) { + NBTTagCompound nbttagcompound = null; + +- try { +- File file = new File(this.playerDir, entityhuman.bu() + ".dat"); +- // Spigot Start +- boolean usingWrongFile = false; +- if ( org.bukkit.Bukkit.getOnlineMode() && !file.exists() ) // Paper - Check online mode first +- { +- file = new File( this.playerDir, UUID.nameUUIDFromBytes( ( "OfflinePlayer:" + entityhuman.getName() ).getBytes( "UTF-8" ) ).toString() + ".dat"); +- if ( file.exists() ) ++ File file = new File(this.playerDir, entityhuman.bu() + ".dat"); ++ if(!MinecraftServer.getServer().isRedisEnabled()) { ++ try { ++ // Spigot Start ++ boolean usingWrongFile = false; ++ if ( org.bukkit.Bukkit.getOnlineMode() && !file.exists() ) // Paper - Check online mode first + { +- usingWrongFile = true; +- org.bukkit.Bukkit.getServer().getLogger().warning( "Using offline mode UUID file for player " + entityhuman.getName() + " as it is the only copy we can find." ); ++ file = new File( this.playerDir, UUID.nameUUIDFromBytes( ( "OfflinePlayer:" + entityhuman.getName() ).getBytes( "UTF-8" ) ).toString() + ".dat"); ++ if ( file.exists() ) ++ { ++ usingWrongFile = true; ++ org.bukkit.Bukkit.getServer().getLogger().warning( "Using offline mode UUID file for player " + entityhuman.getName() + " as it is the only copy we can find." ); ++ } + } +- } +- // Spigot End ++ // Spigot End + +- if (file.exists() && file.isFile()) { +- nbttagcompound = NBTCompressedStreamTools.a((InputStream) (new FileInputStream(file))); ++ if (file.exists() && file.isFile()) { ++ nbttagcompound = NBTCompressedStreamTools.a((InputStream) (new FileInputStream(file))); ++ } ++ // Spigot Start ++ if ( usingWrongFile ) ++ { ++ file.renameTo( new File( file.getPath() + ".offline-read" ) ); ++ } ++ // Spigot End ++ } catch (Exception exception) { ++ WorldNBTStorage.b.warn("Failed to load player data for {}", entityhuman.getDisplayName().getString()); + } +- // Spigot Start +- if ( usingWrongFile ) +- { +- file.renameTo( new File( file.getPath() + ".offline-read" ) ); ++ } else { ++ String key = WorldFileUtils.getRedisKeyFromRegionFile(file); ++ RBucket playerData = this.redissonClient.getBucket(key); ++ try { ++ boolean usingWrongFile = false; ++ if ( org.bukkit.Bukkit.getOnlineMode() && !playerData.isExists() ) // Paper - Check online mode first ++ { ++ file = new File( this.playerDir, UUID.nameUUIDFromBytes( ( "OfflinePlayer:" + entityhuman.getName() ).getBytes( "UTF-8" ) ).toString() + ".dat"); ++ key = WorldFileUtils.getRedisKeyFromRegionFile(file); ++ playerData = this.redissonClient.getBucket(key); ++ if ( playerData.isExists() ) ++ { ++ usingWrongFile = true; ++ org.bukkit.Bukkit.getServer().getLogger().warning( "Using offline mode UUID file for player " + entityhuman.getName() + " as it is the only copy we can find." ); ++ } ++ } ++ // Spigot End ++ ++ if (playerData.isExists()) { ++ RBinaryStream playerDataStream = this.redissonClient.getBinaryStream(key); ++ nbttagcompound = NBTCompressedStreamTools.a(playerDataStream.getInputStream()); ++ } ++ // Spigot Start ++ if ( usingWrongFile ) ++ { ++ File file1 = new File(file.getPath() + ".offline-read"); ++ String key1 = WorldFileUtils.getRedisKeyFromRegionFile(file1); ++ playerData.rename(key1); ++ } ++ } catch (Exception exception) { ++ WorldNBTStorage.b.warn("Failed to load player data for {}", entityhuman.getDisplayName().getString()); + } +- // Spigot End +- } catch (Exception exception) { +- WorldNBTStorage.b.warn("Failed to load player data for {}", entityhuman.getDisplayName().getString()); + } + + if (nbttagcompound != null) { +@@ -248,7 +362,11 @@ public class WorldNBTStorage implements IDataManager, IPlayerFileData { + if (entityhuman instanceof EntityPlayer) { + CraftPlayer player = (CraftPlayer) entityhuman.getBukkitEntity(); + // Only update first played if it is older than the one we have +- long modified = new File(this.playerDir, entityhuman.getUniqueID().toString() + ".dat").lastModified(); ++ long modified = Long.MAX_VALUE; ++ if (!MinecraftServer.getServer().isRedisEnabled()) { ++ modified = new File(this.playerDir, entityhuman.getUniqueID().toString() + ".dat").lastModified(); ++ } ++ + if (modified < player.getFirstPlayed()) { + player.setFirstPlayed(modified); + } +@@ -283,19 +401,29 @@ public class WorldNBTStorage implements IDataManager, IPlayerFileData { + } + + public String[] getSeenPlayers() { +- String[] astring = this.playerDir.list(); ++ if(MinecraftServer.getServer().isRedisEnabled()) { // Paper ++ String[] astring = this.playerDir.list(); + +- if (astring == null) { +- astring = new String[0]; +- } ++ if (astring == null) { ++ astring = new String[0]; ++ } + +- for (int i = 0; i < astring.length; ++i) { +- if (astring[i].endsWith(".dat")) { +- astring[i] = astring[i].substring(0, astring[i].length() - 4); ++ for (int i = 0; i < astring.length; ++i) { ++ if (astring[i].endsWith(".dat")) { ++ astring[i] = astring[i].substring(0, astring[i].length() - 4); ++ } + } +- } + +- return astring; ++ return astring; ++ } else { // Paper start ++ String pattern = WorldFileUtils.getRedisKeyFromRegionFile(this.playerDir) + ":*.json"; ++ System.out.println("pattern = " + pattern); ++ Iterable keys = this.redissonClient.getKeys().getKeysByPattern(pattern); ++ List list = StreamSupport ++ .stream(keys.spliterator(), false) ++ .collect(Collectors.toList()); ++ return (String[]) list.toArray(); ++ } // Paper end + } + + public void a() {} +@@ -303,7 +431,9 @@ public class WorldNBTStorage implements IDataManager, IPlayerFileData { + public File getDataFile(DimensionManager dimensionmanager, String s) { + File file = new File(dimensionmanager.a(this.baseDir), "data"); + +- file.mkdirs(); ++ if(!MinecraftServer.getServer().isRedisEnabled()) { // Paper ++ file.mkdirs(); ++ } + return new File(file, s + ".dat"); + } + +@@ -319,10 +449,28 @@ public class WorldNBTStorage implements IDataManager, IPlayerFileData { + public UUID getUUID() { + if (uuid != null) return uuid; + File file1 = new File(this.baseDir, "uid.dat"); +- if (file1.exists()) { ++ ++ // Paper start ++ String key = WorldFileUtils.getRedisKeyFromRegionFile(file1); ++ boolean fileKeyExists = false; ++ if (MinecraftServer.getServer().isRedisEnabled()) { ++ fileKeyExists = redissonClient.getBucket(key).isExists(); ++ } else { ++ fileKeyExists = file1.exists(); ++ } // Paper end ++ ++ if (fileKeyExists) { + DataInputStream dis = null; + try { +- dis = new DataInputStream(new FileInputStream(file1)); ++ // Paper start ++ InputStream in; ++ if (!MinecraftServer.getServer().isRedisEnabled()) { ++ in = new FileInputStream(file1); ++ } else { ++ in = redissonClient.getBinaryStream(key).getInputStream(); ++ } ++ // Paper end ++ dis = new DataInputStream(in); + return uuid = new UUID(dis.readLong(), dis.readLong()); + } catch (IOException ex) { + b.warn("Failed to read " + file1 + ", generating new random UUID", ex); +@@ -339,7 +487,15 @@ public class WorldNBTStorage implements IDataManager, IPlayerFileData { + uuid = UUID.randomUUID(); + DataOutputStream dos = null; + try { +- dos = new DataOutputStream(new FileOutputStream(file1)); ++ // Paper start ++ OutputStream out; ++ if (!MinecraftServer.getServer().isRedisEnabled()) { ++ out = new FileOutputStream(file1); ++ } else { ++ out = redissonClient.getBinaryStream(key).getOutputStream(); ++ } ++ // Paper end ++ dos = new DataOutputStream(out); + dos.writeLong(uuid.getMostSignificantBits()); + dos.writeLong(uuid.getLeastSignificantBits()); + } catch (IOException ex) { +diff --git a/src/main/java/net/minecraft/server/WorldPersistentData.java b/src/main/java/net/minecraft/server/WorldPersistentData.java +index e86d382c..1c128822 100644 +--- a/src/main/java/net/minecraft/server/WorldPersistentData.java ++++ b/src/main/java/net/minecraft/server/WorldPersistentData.java +@@ -1,5 +1,6 @@ + package net.minecraft.server; + ++import com.destroystokyo.paper.util.WorldFileUtils; + import com.google.common.collect.Maps; + import com.mojang.datafixers.DataFixTypes; + import it.unimi.dsi.fastutil.objects.Object2IntMap; +@@ -21,6 +22,7 @@ import java.util.function.Function; + import javax.annotation.Nullable; + import org.apache.logging.log4j.LogManager; + import org.apache.logging.log4j.Logger; ++import org.redisson.api.RedissonClient; + + public class WorldPersistentData { + +@@ -30,11 +32,13 @@ public class WorldPersistentData { + private final Object2IntMap d = new Object2IntOpenHashMap(); + @Nullable + private final IDataManager e; ++ private final RedissonClient redissonClient; + + public WorldPersistentData(DimensionManager dimensionmanager, @Nullable IDataManager idatamanager) { + this.b = dimensionmanager; + this.e = idatamanager; + this.d.defaultReturnValue(-1); ++ this.redissonClient = MinecraftServer.getServer().getRedissonClient(); + } + + @Nullable +@@ -72,9 +76,16 @@ public class WorldPersistentData { + } + + File file = this.e.getDataFile(this.b, "idcounts"); ++ String key = WorldFileUtils.getRedisKeyFromRegionFile(file); + +- if (file != null && file.exists()) { +- DataInputStream datainputstream = new DataInputStream(new FileInputStream(file)); ++ if ((file != null && file.exists()) || (MinecraftServer.getServer().isRedisEnabled() && redissonClient.getBucket(key).isExists())) { ++ InputStream in; ++ if (MinecraftServer.getServer().isRedisEnabled()) { ++ in = this.redissonClient.getBinaryStream(key).getInputStream(); ++ } else { ++ in = new FileInputStream(file); ++ } ++ DataInputStream datainputstream = new DataInputStream(in); + NBTTagCompound nbttagcompound = NBTCompressedStreamTools.a(datainputstream); + + datainputstream.close(); +@@ -88,6 +99,7 @@ public class WorldPersistentData { + } + } + } ++ + } catch (Exception exception) { + WorldPersistentData.a.error("Could not load aux values", exception); + } +@@ -114,7 +126,14 @@ public class WorldPersistentData { + nbttagcompound.setInt((String) entry.getKey(), entry.getIntValue()); + } + +- DataOutputStream dataoutputstream = new DataOutputStream(new FileOutputStream(file)); ++ OutputStream out; ++ if (!MinecraftServer.getServer().isRedisEnabled()) { ++ out = new FileOutputStream(file); ++ } else { ++ String key = WorldFileUtils.getRedisKeyFromRegionFile(file); ++ out = this.redissonClient.getBinaryStream(key).getOutputStream(); ++ } ++ DataOutputStream dataoutputstream = new DataOutputStream(out); + + NBTCompressedStreamTools.a(nbttagcompound, (DataOutput) dataoutputstream); + dataoutputstream.close(); +@@ -130,7 +149,13 @@ public class WorldPersistentData { + public static NBTTagCompound a(IDataManager idatamanager, DimensionManager dimensionmanager, String s, int i) throws IOException { + if ("Mineshaft".equals(s) || "Mineshaft_index".equals(s)) return new NBTTagCompound(); // Paper + File file = idatamanager.getDataFile(dimensionmanager, s); +- FileInputStream fileinputstream = new FileInputStream(file); ++ InputStream fileinputstream; ++ if (!MinecraftServer.getServer().isRedisEnabled()) { ++ fileinputstream = new FileInputStream(file); ++ } else { ++ String key = WorldFileUtils.getRedisKeyFromRegionFile(file); ++ fileinputstream = MinecraftServer.getServer().getRedissonClient().getBinaryStream(key).getInputStream(); ++ } + Throwable throwable = null; + + NBTTagCompound nbttagcompound; +@@ -188,9 +213,16 @@ public class WorldPersistentData { + + nbttagcompound.set("data", persistentbase.b(new NBTTagCompound())); + nbttagcompound.setInt("DataVersion", 1631); +- FileOutputStream fileoutputstream = new FileOutputStream(file); +- +- NBTCompressedStreamTools.a(nbttagcompound, (OutputStream) fileoutputstream); ++ // Paper start ++ OutputStream fileoutputstream; ++ if (!MinecraftServer.getServer().isRedisEnabled()) { ++ fileoutputstream = new FileOutputStream(file); ++ } else { ++ String key = WorldFileUtils.getRedisKeyFromRegionFile(file); ++ fileoutputstream = MinecraftServer.getServer().getRedissonClient().getBinaryStream(key).getOutputStream(); ++ } ++ // Paper end ++ NBTCompressedStreamTools.a(nbttagcompound, fileoutputstream); + fileoutputstream.close(); + } + } catch (Exception exception) { +diff --git a/src/main/resources/configurations/redis.yml b/src/main/resources/configurations/redis.yml +new file mode 100644 +index 00000000..0b51e7b4 +--- /dev/null ++++ b/src/main/resources/configurations/redis.yml +@@ -0,0 +1,21 @@ ++--- ++singleServerConfig: ++ idleConnectionTimeout: 10000 ++ connectTimeout: 10000 ++ timeout: 3000 ++ retryAttempts: 3 ++ retryInterval: 1500 ++ password: null ++ subscriptionsPerConnection: 5 ++ clientName: null ++ address: "redis://127.0.0.1:6379" ++ subscriptionConnectionMinimumIdleSize: 1 ++ subscriptionConnectionPoolSize: 50 ++ connectionMinimumIdleSize: 24 ++ connectionPoolSize: 64 ++ database: 0 ++ dnsMonitoringInterval: 5000 ++threads: 16 ++nettyThreads: 32 ++codec: ! {} ++transportMode: "NIO" +\ No newline at end of file +-- +2.22.0.windows.1 +