├── .gitignore
├── .idea
├── .gitignore
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── encodings.xml
├── hyperblock-java.iml
├── inspectionProfiles
│ └── Project_Default.xml
├── jarRepositories.xml
├── misc.xml
├── modules.xml
├── sbt.xml
└── vcs.xml
├── README.md
├── build.sh
├── hyper-plugin
├── pom.xml
└── src
│ └── main
│ ├── java
│ └── gg
│ │ └── amy
│ │ └── hyperblock
│ │ ├── Agent.java
│ │ ├── Hyperblock.java
│ │ ├── bukkit
│ │ ├── SkyblockChunkGenerator.java
│ │ ├── SkyblockPopulator.java
│ │ └── WorldStorageBackend.java
│ │ ├── bytecode
│ │ ├── Injector.java
│ │ └── injectors
│ │ │ └── RegionFileCacheInjector.java
│ │ ├── command
│ │ └── IamCommand.java
│ │ ├── component
│ │ ├── Database.java
│ │ └── database
│ │ │ ├── HyperPlayer.java
│ │ │ └── Snapshot.java
│ │ ├── listener
│ │ └── PlayerOnlineListener.java
│ │ └── utils
│ │ ├── NbtHelpers.java
│ │ └── ThrowingSupplier.java
│ └── resources
│ ├── META-INF
│ └── MANIFEST.MF
│ └── plugin.yml
├── libhyper
├── pom.xml
└── src
│ └── main
│ └── java
│ └── gg
│ └── amy
│ ├── hyperblock
│ └── lib
│ │ └── LibHyper.java
│ └── singyeong
│ ├── SingyeongClient.java
│ ├── client
│ ├── SingyeongMessage.java
│ ├── SingyeongOp.java
│ ├── SingyeongSocket.java
│ ├── SingyeongType.java
│ └── query
│ │ ├── Query.java
│ │ └── QueryBuilder.java
│ ├── data
│ ├── Dispatch.java
│ ├── Invalid.java
│ └── ProxiedRequest.java
│ └── util
│ └── JsonPojoCodec.java
└── pom.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | *.class
2 | *.log
3 | *.ctxt
4 | .mtj.tmp/
5 | *.jar
6 | *.war
7 | *.nar
8 | *.ear
9 | *.zip
10 | *.tar.gz
11 | *.rar
12 | hs_err_pid*
13 | target/
14 | pom.xml.tag
15 | pom.xml.releaseBackup
16 | pom.xml.versionsBackup
17 | pom.xml.next
18 | release.properties
19 | dependency-reduced-pom.xml
20 | buildNumber.properties
21 | .mvn/timing.properties
22 | .mvn/wrapper/maven-wrapper.jar
23 | .idea/**/workspace.xml
24 | .idea/**/tasks.xml
25 | .idea/**/usage.statistics.xml
26 | .idea/**/dictionaries
27 | .idea/**/shelf
28 | .idea/**/contentModel.xml
29 | .idea/**/dataSources/
30 | .idea/**/dataSources.ids
31 | .idea/**/dataSources.local.xml
32 | .idea/**/sqlDataSources.xml
33 | .idea/**/dynamic.xml
34 | .idea/**/uiDesigner.xml
35 | .idea/**/dbnavigator.xml
36 | .idea/artifacts
37 | .idea/compiler.xml
38 | .idea/jarRepositories.xml
39 | .idea/modules.xml
40 | .idea/*.iml
41 | .idea/modules
42 | *.iml
43 | *.ipr
44 | .idea/**/mongoSettings.xml
45 | *.iws
46 | out/
47 | .idea_modules/
48 | atlassian-ide-plugin.xml
49 | .idea/replstate.xml
50 | com_crashlytics_export_strings.xml
51 | crashlytics.properties
52 | crashlytics-build.properties
53 | fabric.properties
54 | .idea/httpRequests
55 | .idea/caches/build_file_checksums.ser
56 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/hyperblock-java.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 | com.mewna.catnip.entity.user.User,com.grack.nanojson.JsonObject,com.grack.nanojson.JsonArray,com.mewna.catnip.rest.guild.GuildData,com.mewna.catnip.rest.guild.ChannelData,com.mewna.catnip.rest.guild.RoleData,java.lang.String,com.mewna.catnip.entity.guild.Member,com.mewna.catnip.entity.guild.PermissionHolder,com.mewna.catnip.entity.guild.Role,java.util.Collection,com.mewna.catnip.rest.invite.InviteCreateOptions,com.mewna.catnip.entity.guild.PermissionOverride
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | IDE
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/sbt.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # hyperblock
2 |
3 | A theoretically-infinitely-scalable Skyblock plugin.
4 |
5 | ## wat
6 |
7 | Worlds are persisted in S3 buckets, write-through cached in Redis. Player data
8 | lives in MongoDB because let's be real, doing anything SQL with Java fucking
9 | sucks.
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | env MAVEN_OPTS="--illegal-access=permit" mvn clean package
4 |
--------------------------------------------------------------------------------
/hyper-plugin/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | hyper-plugin
8 | 0.0.1
9 |
10 |
11 | 16
12 | 16
13 |
14 |
15 |
16 | hyperblock
17 | gg.amy.hyperblock
18 | 0.0.1
19 |
20 |
21 |
22 |
23 | jitpack
24 | jitpack
25 | https://jitpack.io/
26 | default
27 |
28 |
29 |
30 |
31 |
32 | gg.amy.hyperblock
33 | libhyper
34 | ${project.version}
35 |
36 |
37 |
38 | org.spigotmc
39 | spigot
40 | 1.16.5-R0.1-SNAPSHOT
41 | provided
42 |
43 |
44 |
45 | com.github.queer
46 | cardboard
47 | 91d275d
48 |
49 |
50 | org.spigotmc
51 | spigot-api
52 |
53 |
54 |
55 | com.github.MilkBowl
56 | VaultAPI
57 |
58 |
59 |
60 |
61 |
62 | redis.clients
63 | jedis
64 | 3.5.2
65 |
66 |
67 |
68 | com.github.luben
69 | zstd-jni
70 | 1.4.9-1
71 |
72 |
73 |
74 | org.ow2.asm
75 | asm
76 | 9.1
77 |
78 |
79 |
80 | org.ow2.asm
81 | asm-tree
82 | 9.1
83 |
84 |
85 |
86 | org.ow2.asm
87 | asm-util
88 | 9.1
89 |
90 |
91 |
92 | com.github.hervian
93 | safety-mirror
94 | 4.0.1
95 |
96 |
97 |
98 | software.amazon.awssdk
99 | s3
100 | 2.16.26
101 |
102 |
103 |
104 | commons-io
105 | commons-io
106 | 2.8.0
107 |
108 |
109 |
110 | org.mongodb
111 | mongodb-driver-sync
112 | 4.2.2
113 |
114 |
115 |
116 |
117 | hyperblock
118 |
119 |
120 | src/main/resources
121 | true
122 |
123 |
124 |
125 |
126 |
127 | org.apache.maven.plugins
128 | maven-shade-plugin
129 | 3.2.4
130 |
131 |
132 | package
133 |
134 | shade
135 |
136 |
137 |
138 |
139 | *:*:*:sources:*
140 | *:*:*:javadoc:*
141 | org.projectlombok:*:*:*:*
142 | org.spigotmc:*:*:*:*
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | org.apache.maven.plugins
152 | maven-jar-plugin
153 | 3.2.0
154 |
155 |
156 | src/main/resources/META-INF/MANIFEST.MF
157 |
158 |
159 |
160 |
161 |
162 |
163 |
--------------------------------------------------------------------------------
/hyper-plugin/src/main/java/gg/amy/hyperblock/Agent.java:
--------------------------------------------------------------------------------
1 | package gg.amy.hyperblock;
2 |
3 | import gg.amy.hyperblock.bytecode.Injector;
4 | import gg.amy.hyperblock.bytecode.injectors.RegionFileCacheInjector;
5 |
6 | import java.lang.instrument.Instrumentation;
7 | import java.lang.instrument.UnmodifiableClassException;
8 | import java.util.Arrays;
9 | import java.util.List;
10 | import java.util.stream.Collectors;
11 |
12 | /**
13 | * @author amy
14 | * @since 3/22/21.
15 | */
16 | public final class Agent {
17 | private static final List INJECTORS = List.of(
18 | new RegionFileCacheInjector()
19 | );
20 |
21 | private Agent() {
22 | }
23 |
24 | public static void agentmain(final String agentArgs, final Instrumentation inst) {
25 | System.out.println(">> agent: agentmain: start");
26 | System.out.println(">> agent: agentmain: can transform? " + inst.isRetransformClassesSupported());
27 | for(final var injector : INJECTORS) {
28 | inst.addTransformer(injector);
29 | }
30 | try {
31 | final var classes = INJECTORS.stream()
32 | .map(Injector::getClassToInject)
33 | .map(c -> {
34 | try {
35 | return Class.forName(c.replace('/', '.'));
36 | } catch(final ClassNotFoundException e) {
37 | throw new RuntimeException(e);
38 | }
39 | })
40 | .collect(Collectors.toList())
41 | .toArray(Class[]::new);
42 |
43 | System.out.println(">> agent: agentmain: asking to retransform: " + Arrays.toString(classes));
44 | inst.retransformClasses(classes);
45 | } catch(final UnmodifiableClassException e) {
46 | throw new RuntimeException(e);
47 | }
48 | System.out.println(">> agent: agentmain: finish");
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/hyper-plugin/src/main/java/gg/amy/hyperblock/Hyperblock.java:
--------------------------------------------------------------------------------
1 | package gg.amy.hyperblock;
2 |
3 | import com.sun.tools.attach.AgentInitializationException;
4 | import com.sun.tools.attach.AgentLoadException;
5 | import com.sun.tools.attach.AttachNotSupportedException;
6 | import com.sun.tools.attach.VirtualMachine;
7 | import gg.amy.hyperblock.bukkit.SkyblockChunkGenerator;
8 | import gg.amy.mc.cardboard.Cardboard;
9 | import org.bukkit.generator.ChunkGenerator;
10 | import org.jetbrains.annotations.NotNull;
11 | import org.jetbrains.annotations.Nullable;
12 |
13 | import java.io.IOException;
14 | import java.lang.management.ManagementFactory;
15 |
16 | /**
17 | * @author amy
18 | * @since 3/23/21.
19 | */
20 | public class Hyperblock extends Cardboard {
21 | public Hyperblock() {
22 | try {
23 | final VirtualMachine m = VirtualMachine.attach(ManagementFactory.getRuntimeMXBean().getPid() + "");
24 | m.loadAgent(System.getProperty("user.dir") + "/plugins/hyperblock.jar");
25 | } catch(final AttachNotSupportedException | AgentInitializationException | AgentLoadException | IOException e) {
26 | e.printStackTrace();
27 | }
28 | }
29 |
30 | @Nullable
31 | @Override
32 | public ChunkGenerator getDefaultWorldGenerator(@NotNull final String worldName, @Nullable final String id) {
33 | return new SkyblockChunkGenerator();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/hyper-plugin/src/main/java/gg/amy/hyperblock/bukkit/SkyblockChunkGenerator.java:
--------------------------------------------------------------------------------
1 | package gg.amy.hyperblock.bukkit;
2 |
3 | import org.bukkit.Location;
4 | import org.bukkit.Material;
5 | import org.bukkit.World;
6 | import org.bukkit.block.Biome;
7 | import org.bukkit.generator.BlockPopulator;
8 | import org.bukkit.generator.ChunkGenerator;
9 | import org.jetbrains.annotations.NotNull;
10 | import org.jetbrains.annotations.Nullable;
11 |
12 | import javax.annotation.Nonnull;
13 | import java.util.List;
14 | import java.util.Random;
15 |
16 | /**
17 | * @author amy
18 | * @since 3/23/21.
19 | */
20 | public class SkyblockChunkGenerator extends ChunkGenerator {
21 | private final List populators = List.of(new SkyblockPopulator());
22 |
23 | @Nonnull
24 | @Override
25 | public ChunkData generateChunkData(@Nonnull final World world, @Nonnull final Random random, final int x,
26 | final int z, @Nonnull final BiomeGrid biome) {
27 | final var chunk = createChunkData(world);
28 | for(int ix = 0; ix < 16; ix++) {
29 | for(int iy = 0; iy < chunk.getMaxHeight(); iy++) {
30 | for(int iz = 0; iz < 16; iz++) {
31 | chunk.setBlock(ix, iy, iz, Material.AIR);
32 | biome.setBiome(ix, iy, iz, Biome.OCEAN);
33 | }
34 | }
35 | }
36 |
37 | if(x == 0 && z == 0) {
38 | // If this is the spawn chunk, build out a little platform
39 | for(int ix = 0; ix < 5; ix++) {
40 | for(int iz = 0; iz < 5; iz++) {
41 | chunk.setBlock(6 + ix, 127, 6 + iz, Material.GRASS_BLOCK);
42 | chunk.setBlock(6 + ix, 126, 6 + iz, Material.DIRT);
43 | chunk.setBlock(6 + ix, 126, 6 + iz, Material.DIRT);
44 | }
45 | }
46 |
47 | // Add leaves for our tree
48 | for(int ix = 0; ix < 5; ix++) {
49 | for(int iz = 0; iz < 5; iz++) {
50 | chunk.setBlock(7 + ix, 130, 7 + iz, Material.OAK_LEAVES);
51 | chunk.setBlock(7 + ix, 131, 7 + iz, Material.OAK_LEAVES);
52 | chunk.setBlock(7 + ix, 132, 7 + iz, Material.OAK_LEAVES);
53 | }
54 | }
55 | for(int ix = 0; ix < 3; ix++) {
56 | for(int iz = 0; iz < 3; iz++) {
57 | chunk.setBlock(7 + ix, 133, 7 + iz, Material.OAK_LEAVES);
58 | chunk.setBlock(7 + ix, 134, 7 + iz, Material.OAK_LEAVES);
59 | }
60 | }
61 | chunk.setBlock(8, 135, 8, Material.OAK_LEAVES);
62 |
63 | // Then add the logs
64 | chunk.setBlock(8, 128, 8, Material.OAK_LOG);
65 | chunk.setBlock(8, 129, 8, Material.OAK_LOG);
66 | chunk.setBlock(8, 130, 8, Material.OAK_LOG);
67 | chunk.setBlock(8, 131, 8, Material.OAK_LOG);
68 | chunk.setBlock(8, 132, 8, Material.OAK_LOG);
69 | }
70 | return chunk;
71 | }
72 |
73 | @NotNull
74 | @Override
75 | public List getDefaultPopulators(@NotNull final World world) {
76 | return populators;
77 | }
78 |
79 | @Nullable
80 | @Override
81 | public Location getFixedSpawnLocation(@NotNull final World world, @NotNull final Random random) {
82 | return new Location(world, 0.5D, 128, 0.5D);
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/hyper-plugin/src/main/java/gg/amy/hyperblock/bukkit/SkyblockPopulator.java:
--------------------------------------------------------------------------------
1 | package gg.amy.hyperblock.bukkit;
2 |
3 | import org.bukkit.Chunk;
4 | import org.bukkit.Location;
5 | import org.bukkit.Material;
6 | import org.bukkit.World;
7 | import org.bukkit.block.Chest;
8 | import org.bukkit.generator.BlockPopulator;
9 | import org.bukkit.inventory.ItemStack;
10 | import org.jetbrains.annotations.NotNull;
11 |
12 | import java.util.Random;
13 |
14 | /**
15 | * @author amy
16 | * @since 3/24/21.
17 | */
18 | public class SkyblockPopulator extends BlockPopulator {
19 | @Override
20 | public void populate(@NotNull final World world, @NotNull final Random random, @NotNull final Chunk chunk) {
21 | if(chunk.getX() == 0 && chunk.getZ() == 0) {
22 | System.out.println(">> hyper: worldgen: populating spawn chunk");
23 | final var loc = new Location(world, 9, 128, 9);
24 | loc.getBlock().setType(Material.CHEST);
25 | final var state = (Chest) loc.getBlock().getState();
26 | final var inv = state.getBlockInventory();
27 | inv.addItem(
28 | new ItemStack(Material.COBWEB, 12),
29 | new ItemStack(Material.RED_MUSHROOM, 1),
30 | new ItemStack(Material.BROWN_MUSHROOM, 1),
31 | new ItemStack(Material.LAVA_BUCKET, 1),
32 | new ItemStack(Material.ICE, 2),
33 | new ItemStack(Material.BONE_MEAL, 2),
34 | new ItemStack(Material.PUMPKIN_SEEDS, 1),
35 | new ItemStack(Material.CACTUS, 1),
36 | new ItemStack(Material.SUGAR_CANE, 1)
37 | );
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/hyper-plugin/src/main/java/gg/amy/hyperblock/bukkit/WorldStorageBackend.java:
--------------------------------------------------------------------------------
1 | package gg.amy.hyperblock.bukkit;
2 |
3 | import com.github.luben.zstd.Zstd;
4 | import gg.amy.hyperblock.utils.NbtHelpers;
5 | import gg.amy.hyperblock.utils.ThrowingSupplier;
6 | import net.minecraft.server.v1_16_R3.ChunkCoordIntPair;
7 | import net.minecraft.server.v1_16_R3.NBTTagCompound;
8 | import org.bukkit.Bukkit;
9 | import org.bukkit.craftbukkit.v1_16_R3.CraftServer;
10 | import redis.clients.jedis.Jedis;
11 | import redis.clients.jedis.JedisPool;
12 | import redis.clients.jedis.JedisPoolConfig;
13 | import redis.clients.jedis.Transaction;
14 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
15 | import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
16 | import software.amazon.awssdk.core.sync.RequestBody;
17 | import software.amazon.awssdk.regions.Region;
18 | import software.amazon.awssdk.services.s3.S3Client;
19 | import software.amazon.awssdk.services.s3.model.*;
20 |
21 | import javax.annotation.Nonnull;
22 | import java.io.ByteArrayInputStream;
23 | import java.io.ByteArrayOutputStream;
24 | import java.io.File;
25 | import java.net.URI;
26 | import java.util.Collection;
27 | import java.util.LinkedList;
28 | import java.util.concurrent.ExecutorService;
29 | import java.util.concurrent.Executors;
30 | import java.util.concurrent.Future;
31 | import java.util.function.Consumer;
32 | import java.util.function.Function;
33 |
34 | /**
35 | * @author amy
36 | * @since 3/22/21.
37 | */
38 | @SuppressWarnings({"unused", "OptionalGetWithoutIsPresent"})
39 | public final class WorldStorageBackend {
40 | private static final JedisPool POOL;
41 | private static final Collection CMP = new LinkedList<>();
42 | private static final String WRITE_QUEUE = "'hyperblock:queue:worlds:dirty-chunks'";
43 | private static final S3Client S;
44 | private static final ExecutorService BACKGROUND_WRITER = Executors.newFixedThreadPool(1);
45 | private static final Future> BACKGROUND_WRITER_FUTURE;
46 | private static final String BUCKET = "hyperblock-chunk-store";
47 |
48 | static {
49 | final var config = new JedisPoolConfig();
50 | config.setMaxTotal(10);
51 | config.setMaxIdle(3);
52 | // TODO: Env
53 | POOL = new JedisPool(config);
54 |
55 | S = S3Client.builder()
56 | .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials.create("minioadmin", "minioadmin")))
57 | // TODO: Env
58 | .endpointOverride(URI.create("http://localhost:9000"))
59 | .region(Region.AWS_GLOBAL)
60 | .build();
61 | try {
62 | S.createBucket(CreateBucketRequest.builder()
63 | .bucket(BUCKET)
64 | .build());
65 | } catch(final BucketAlreadyExistsException | BucketAlreadyOwnedByYouException e) {
66 | System.out.println(">> backend: storage: bucket already exists");
67 | }
68 | S.waiter().waitUntilBucketExists(HeadBucketRequest.builder()
69 | .bucket(BUCKET)
70 | .build());
71 |
72 | BACKGROUND_WRITER_FUTURE = BACKGROUND_WRITER.submit(() -> {
73 | while(true) {
74 | if(!((CraftServer) Bukkit.getServer()).getServer().isRunning()) {
75 | return;
76 | }
77 | redis(r -> {
78 | try {
79 | final var dirtyChunk = r.blpop(0, WRITE_QUEUE);
80 | final var dirtyKey = dirtyChunk.get(1);
81 | // System.out.println(">> backend: storage: dirty chunk: " + dirtyKey);
82 | final var compressedDirtyChunk = r.get(dirtyKey.getBytes());
83 | s(() -> {
84 | // TODO: Versioning
85 | S.putObject(
86 | PutObjectRequest.builder()
87 | .key(keyToS3(dirtyKey))
88 | .bucket(BUCKET)
89 | .build(),
90 | RequestBody.fromBytes(compressedDirtyChunk)
91 | );
92 | return null;
93 | });
94 | } catch(final Exception e) {
95 | e.printStackTrace();
96 | }
97 | });
98 | }
99 | });
100 |
101 | Runtime.getRuntime().addShutdownHook(new Thread(() -> {
102 | final var average = CMP.stream().mapToInt(i -> i).average().getAsDouble();
103 | final var max = CMP.stream().mapToInt(i -> i).max().getAsInt();
104 | System.out.println(">> backend: storage: avg size bytes = " + Math.round(average) + ", max size bytes = " + max);
105 | }));
106 | }
107 |
108 | private WorldStorageBackend() {
109 | }
110 |
111 | private static void cancelBackgroundWriter() {
112 | BACKGROUND_WRITER_FUTURE.cancel(true);
113 | }
114 |
115 | /**
116 | * Load a chunk. This bypasses the whole "region file" thing that Minecraft
117 | * does, as I honestly can't be bothered to figure it out. Instead, we can
118 | * just inspect the directory we're SUPPOSED to read from to find out what
119 | * world this is, then load it from our remote datastore manually.
120 | *
121 | * @param file The directory it's SUPPOSED to be in.
122 | * @param coords The coordinates of the chunk.
123 | * @return Some valid NBT idk
124 | */
125 | @SuppressWarnings({"UnusedReturnValue", "unused"}) // bytecode injection
126 | public static NBTTagCompound read(@Nonnull final File file, @Nonnull final ChunkCoordIntPair coords) {
127 | final var key = fileToKey(file, coords);
128 | final var exists = redis(r -> {
129 | return r.exists(key);
130 | });
131 | final byte[] bytes;
132 | if(!exists) {
133 | bytes = s(() -> {
134 | try(final var stream = S.getObject(GetObjectRequest.builder()
135 | .bucket(BUCKET)
136 | .key(keyToS3(key))
137 | .build())) {
138 | return stream.readAllBytes();
139 | } catch(final NoSuchKeyException ignored) {
140 | return null;
141 | }
142 | });
143 | if(bytes != null) {
144 | // Write to Redis cache
145 | redis(r -> {
146 | r.set(key.getBytes(), bytes);
147 | });
148 | }
149 | } else {
150 | bytes = redis(r -> {
151 | return r.get(key.getBytes());
152 | });
153 | }
154 | if(bytes != null) {
155 | // TODO: Is 4M enough??
156 | return NbtHelpers.read(new ByteArrayInputStream(Zstd.decompress(bytes, 4 * 1024 * 1024)));
157 | } else {
158 | return null;
159 | }
160 | }
161 |
162 | /**
163 | * Writes a chunk. This writes the chunk to Redis then queues it up for
164 | * persisting.
165 | *
166 | * @param file The directory it's SUPPOSED to be in.
167 | * @param coords The coordinates of the chunk.
168 | * @param nbtTagCompound The NBT to save.
169 | */
170 | public static void write(@Nonnull final File file, @Nonnull final ChunkCoordIntPair coords,
171 | @Nonnull final NBTTagCompound nbtTagCompound) {
172 | final var baos = new ByteArrayOutputStream();
173 | NbtHelpers.write(nbtTagCompound, baos);
174 | final var bytes = baos.toByteArray();
175 | final var compressed = Zstd.compress(bytes);
176 | // We get something like 9-12x compression from this
177 | // It's WILD
178 | // System.out.println(">> backend: storage: compressed " + bytes.length + " to " + compressed.length);
179 | CMP.add(bytes.length);
180 | tx(tx -> {
181 | final byte[] key = fileToKey(file, coords).getBytes();
182 | tx.set(key, compressed);
183 | tx.rpush(WRITE_QUEUE.getBytes(), key);
184 | });
185 | }
186 |
187 | private static String fileToKey(@Nonnull final File file, @Nonnull final ChunkCoordIntPair coords) {
188 | // TODO: Actual world name?
189 | final var world = file.getParentFile().getName();
190 | return "hyperblock:worlds:" + world + ":chunks:" + coords.x + '-' + coords.z;
191 | }
192 |
193 | private static String keyToS3(@Nonnull final String key) {
194 | return key.replace(':', '/');
195 | }
196 |
197 | private static void redis(@Nonnull final Consumer f) {
198 | try(@Nonnull final var r = POOL.getResource()) {
199 | f.accept(r);
200 | }
201 | }
202 |
203 | private static T redis(@Nonnull final Function f) {
204 | try(@Nonnull final var r = POOL.getResource()) {
205 | return f.apply(r);
206 | }
207 | }
208 |
209 | private static void tx(@Nonnull final Consumer f) {
210 | redis(r -> {
211 | final var tx = r.multi();
212 | f.accept(tx);
213 | tx.exec();
214 | });
215 | }
216 |
217 | @SuppressWarnings("UnusedReturnValue")
218 | private static T s(final ThrowingSupplier f) {
219 | try {
220 | return f.get();
221 | } catch(final Exception e) {
222 | throw new IllegalStateException(e);
223 | }
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/hyper-plugin/src/main/java/gg/amy/hyperblock/bytecode/Injector.java:
--------------------------------------------------------------------------------
1 | package gg.amy.hyperblock.bytecode;
2 |
3 | import org.objectweb.asm.ClassReader;
4 | import org.objectweb.asm.ClassWriter;
5 | import org.objectweb.asm.Opcodes;
6 | import org.objectweb.asm.tree.ClassNode;
7 | import org.objectweb.asm.util.CheckClassAdapter;
8 |
9 | import java.io.PrintWriter;
10 | import java.io.StringWriter;
11 | import java.lang.instrument.ClassFileTransformer;
12 | import java.security.ProtectionDomain;
13 |
14 | /**
15 | * @author amy
16 | * @since 3/22/21.
17 | */
18 | public abstract class Injector implements ClassFileTransformer, Opcodes {
19 | private final String classToInject;
20 |
21 | protected Injector(final String classToInject) {
22 | System.out.println(">> agent: injector: will inject " + classToInject);
23 | this.classToInject = classToInject;
24 | }
25 |
26 | @SuppressWarnings("SameParameterValue")
27 | protected static String $(final Class> c) {
28 | return $(c.getName());
29 | }
30 |
31 | protected static String $(final String s) {
32 | return s.replace('.', '/');
33 | }
34 |
35 | protected static String $$(final Class> c) {
36 | return $$(c.getName());
37 | }
38 |
39 | protected static String $$(final String s) {
40 | return 'L' + $(s) + ';';
41 | }
42 |
43 | @Override
44 | public final byte[] transform(final ClassLoader classLoader, final String s,
45 | final Class> aClass, final ProtectionDomain protectionDomain, final byte[] bytes) {
46 | if(classToInject.equals(s)) {
47 | try {
48 | System.out.println(">> agent: injector: injecting: " + s);
49 | final ClassReader cr = new ClassReader(bytes);
50 | final ClassNode cn = new ClassNode();
51 | cr.accept(cn, 0);
52 | inject(cr, cn);
53 | System.out.println(">> agent: injector: finshed inject, validating");
54 | final ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
55 | cn.accept(cw);
56 | final byte[] cwBytes = cw.toByteArray();
57 | final var sw = new StringWriter();
58 | final var pw = new PrintWriter(sw);
59 | CheckClassAdapter.verify(new ClassReader(cwBytes), false, pw);
60 | System.out.println(">> agent: injector: verify output: " + sw);
61 | return cwBytes;
62 | } catch(final Throwable t) {
63 | t.printStackTrace();
64 | throw new RuntimeException(t);
65 | }
66 | } else {
67 | return null;
68 | }
69 | }
70 |
71 | protected abstract void inject(ClassReader cr, ClassNode cn);
72 |
73 | public String getClassToInject() {
74 | return classToInject;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/hyper-plugin/src/main/java/gg/amy/hyperblock/bytecode/injectors/RegionFileCacheInjector.java:
--------------------------------------------------------------------------------
1 | package gg.amy.hyperblock.bytecode.injectors;
2 |
3 | import gg.amy.hyperblock.bytecode.Injector;
4 | import org.objectweb.asm.ClassReader;
5 | import org.objectweb.asm.tree.*;
6 |
7 | import java.io.File;
8 | import java.util.HashMap;
9 | import java.util.List;
10 | import java.util.Map;
11 |
12 | /**
13 | * @author amy
14 | * @since 3/22/21.
15 | */
16 | public class RegionFileCacheInjector extends Injector {
17 | private static final String TARGET = $("net.minecraft.server.v1_16_R3.RegionFileCache");
18 |
19 | public RegionFileCacheInjector() {
20 | super(TARGET);
21 | }
22 |
23 | @Override
24 | protected void inject(final ClassReader cr, final ClassNode cn) {
25 | injectLoader(cr, cn);
26 | injectSaver(cr, cn);
27 | }
28 |
29 | private void injectLoader(@SuppressWarnings("unused") final ClassReader cr, final ClassNode cn) {
30 | final var readMethod = cn.methods.stream()
31 | .filter(mn -> mn.name.equals("read"))
32 | .findFirst();
33 | if(readMethod.isEmpty()) {
34 | throw new IllegalStateException("Couldn't find RegionFileCache#read(ChunkCoordIntPair)!?");
35 | }
36 | final var mn = readMethod.get();
37 |
38 | final InsnList insns = new InsnList();
39 | // Directory storing the region files. We use this as a lookup key
40 | insns.add(new VarInsnNode(ALOAD, 0));
41 | insns.add(new FieldInsnNode(GETFIELD, $(TARGET), "b", $$(File.class)));
42 | insns.add(new VarInsnNode(ASTORE, 2));
43 |
44 | // File
45 | insns.add(new VarInsnNode(ALOAD, 2));
46 | // Chunk coords
47 | insns.add(new VarInsnNode(ALOAD, 1));
48 |
49 | insns.add(new MethodInsnNode(INVOKESTATIC, $("gg.amy.hyperblock.bukkit.WorldStorageBackend"), "read",
50 | String.format(
51 | "(%s%s)%s",
52 | $$(File.class),
53 | $$("net.minecraft.server.v1_16_R3.ChunkCoordIntPair"),
54 | $$("net.minecraft.server.v1_16_R3.NBTTagCompound")
55 | ),
56 | false));
57 | insns.add(new InsnNode(ARETURN));
58 | mn.instructions.clear();
59 | mn.instructions.add(insns);
60 | // These methods have try/catch normally, so we make it go away
61 | mn.tryCatchBlocks.clear();
62 | // Dedup local variables
63 | mn.localVariables = List.copyOf(mn.localVariables.stream().