├── .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 | 7 | 8 | 18 | 19 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 162 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 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 | 50 | 51 | 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 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/sbt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 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().>collect(HashMap::new, (m, e) -> m.put(e.name, e), Map::putAll).values()); 64 | System.out.println(">> hyper-agent: inject: loader"); 65 | } 66 | 67 | private void injectSaver(@SuppressWarnings("unused") final ClassReader cr, final ClassNode cn) { 68 | final var writeMethod = cn.methods.stream() 69 | .filter(mn -> mn.name.equals("write")) 70 | .findFirst(); 71 | if(writeMethod.isEmpty()) { 72 | throw new IllegalStateException("Couldn't find RegionFileCache#write(ChunkCoordIntPair, NBTTagCompound)!?"); 73 | } 74 | 75 | final var mn = writeMethod.get(); 76 | final InsnList insns = new InsnList(); 77 | // Directory storing the region files. We use this as a lookup key 78 | insns.add(new VarInsnNode(ALOAD, 0)); 79 | insns.add(new FieldInsnNode(GETFIELD, $(TARGET), "b", $$(File.class))); 80 | insns.add(new VarInsnNode(ASTORE, 3)); 81 | 82 | // File 83 | insns.add(new VarInsnNode(ALOAD, 3)); 84 | // Chunk coords 85 | insns.add(new VarInsnNode(ALOAD, 1)); 86 | // NBT 87 | insns.add(new VarInsnNode(ALOAD, 2)); 88 | 89 | insns.add(new MethodInsnNode(INVOKESTATIC, $("gg.amy.hyperblock.bukkit.WorldStorageBackend"), "write", 90 | String.format( 91 | "(%s%s%s)%s", 92 | $$(File.class), 93 | $$("net.minecraft.server.v1_16_R3.ChunkCoordIntPair"), 94 | $$("net.minecraft.server.v1_16_R3.NBTTagCompound"), 95 | "V" 96 | ), 97 | false)); 98 | insns.add(new InsnNode(RETURN)); 99 | mn.instructions.clear(); 100 | mn.instructions.add(insns); 101 | // These methods have try/catch normally, so we make it go away 102 | mn.tryCatchBlocks.clear(); 103 | // Dedup local variables 104 | mn.localVariables = List.copyOf(mn.localVariables.stream().>collect(HashMap::new, (m, e) -> m.put(e.name, e), Map::putAll).values()); 105 | System.out.println(">> hyper-agent: inject: saver"); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /hyper-plugin/src/main/java/gg/amy/hyperblock/command/IamCommand.java: -------------------------------------------------------------------------------- 1 | package gg.amy.hyperblock.command; 2 | 3 | import gg.amy.mc.cardboard.command.Command; 4 | import gg.amy.mc.cardboard.command.Default; 5 | import gg.amy.mc.cardboard.di.Auto; 6 | import org.bukkit.ChatColor; 7 | import org.bukkit.command.CommandSender; 8 | import org.bukkit.entity.Player; 9 | 10 | import java.util.Objects; 11 | 12 | import static org.bukkit.ChatColor.GRAY; 13 | import static org.bukkit.ChatColor.GREEN; 14 | 15 | /** 16 | * @author amy 17 | * @since 3/24/21. 18 | */ 19 | @Command(name = "iam", permissionNode = "hyperblock.commands.iam") 20 | public class IamCommand { 21 | @Auto 22 | private Player player; 23 | @Auto 24 | private CommandSender sender; 25 | 26 | @Default 27 | public void base(final String cmd, final String[] args) { 28 | if(player == null) { 29 | sender.sendMessage(ChatColor.RED + "You aren't a player."); 30 | return; 31 | } 32 | final var l = player.getLocation(); 33 | sender.sendMessage(String.format(""" 34 | %sYou are: %s%s%s (uuid:%s%s%s) 35 | %sYou are at (%s%s%s, %s%s%s, %s%s%s) in %s%s%s. 36 | """, 37 | GRAY, GREEN, player.getName(), GRAY, GREEN, player.getUniqueId(), GRAY, 38 | GRAY, GREEN, l.getBlockX(), GRAY, GREEN, l.getBlockY(), GRAY, GREEN, l.getBlockZ(), GRAY, 39 | GREEN, Objects.requireNonNull(l.getWorld()).getName(), GRAY 40 | )); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /hyper-plugin/src/main/java/gg/amy/hyperblock/component/Database.java: -------------------------------------------------------------------------------- 1 | package gg.amy.hyperblock.component; 2 | 3 | import com.mongodb.ConnectionString; 4 | import com.mongodb.MongoClientSettings; 5 | import com.mongodb.client.MongoClient; 6 | import com.mongodb.client.MongoClients; 7 | import com.mongodb.client.MongoCollection; 8 | import com.mongodb.client.MongoDatabase; 9 | import com.mongodb.client.model.Filters; 10 | import com.mongodb.client.model.Indexes; 11 | import com.mongodb.client.model.ReplaceOptions; 12 | import gg.amy.hyperblock.component.database.HyperPlayer; 13 | import gg.amy.mc.cardboard.component.Component; 14 | import gg.amy.mc.cardboard.component.Single; 15 | import org.bson.codecs.pojo.PojoCodecProvider; 16 | 17 | import javax.annotation.Nonnull; 18 | import java.util.List; 19 | import java.util.UUID; 20 | 21 | import static org.bson.codecs.configuration.CodecRegistries.fromProviders; 22 | import static org.bson.codecs.configuration.CodecRegistries.fromRegistries; 23 | 24 | /** 25 | * @author amy 26 | * @since 3/25/21. 27 | */ 28 | @Single 29 | @Component(name = "database", description = "hyper's mongodb wrapper") 30 | public class Database { 31 | private final MongoClient client; 32 | private final MongoDatabase db; 33 | private final PlayerDb playerDb; 34 | 35 | public Database() { 36 | final var pojoCodecRegistry = fromRegistries(MongoClientSettings.getDefaultCodecRegistry(), 37 | fromProviders(PojoCodecProvider.builder().automatic(true).build())); 38 | 39 | client = MongoClients.create(MongoClientSettings.builder() 40 | // TODO: Env 41 | .applyConnectionString(new ConnectionString("mongodb://127.0.0.1/hyperblock")) 42 | .retryWrites(true) 43 | .codecRegistry(pojoCodecRegistry) 44 | .build()); 45 | db = client.getDatabase("hyperblock"); 46 | db.createCollection("players"); 47 | db.getCollection("players").createIndex(Indexes.text("uuid")); 48 | 49 | playerDb = new PlayerDb(); 50 | } 51 | 52 | public PlayerDb getPlayerDb() { 53 | return playerDb; 54 | } 55 | 56 | private interface DB { 57 | T get(@Nonnull final K key); 58 | 59 | void set(@Nonnull final T obj); 60 | } 61 | 62 | public class PlayerDb implements DB { 63 | private final MongoCollection collection = db.getCollection("players", HyperPlayer.class); 64 | 65 | @Override 66 | public void set(@Nonnull final HyperPlayer player) { 67 | collection.replaceOne(Filters.eq("uuid", player.uuid().toString()), player, new ReplaceOptions().upsert(true)); 68 | } 69 | 70 | @Override 71 | public HyperPlayer get(@Nonnull final UUID uuid) { 72 | var player = collection.find(Filters.eq("uuid", uuid.toString())).first(); 73 | if(player == null) { 74 | player = new HyperPlayer(uuid, List.of(), new byte[0]); 75 | set(player); 76 | } 77 | return player; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /hyper-plugin/src/main/java/gg/amy/hyperblock/component/database/HyperPlayer.java: -------------------------------------------------------------------------------- 1 | package gg.amy.hyperblock.component.database; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.List; 5 | import java.util.UUID; 6 | 7 | /** 8 | * @author amy 9 | * @since 3/25/21. 10 | */ 11 | public record HyperPlayer(@Nonnull UUID uuid, @Nonnull List snapshots, @Nonnull byte[] compressedInventory) { 12 | } 13 | -------------------------------------------------------------------------------- /hyper-plugin/src/main/java/gg/amy/hyperblock/component/database/Snapshot.java: -------------------------------------------------------------------------------- 1 | package gg.amy.hyperblock.component.database; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.UUID; 5 | 6 | /** 7 | * @author amy 8 | * @since 3/25/21. 9 | */ 10 | public record Snapshot(@Nonnull UUID uuid, @Nonnull String name, @Nonnull String date) { 11 | } 12 | -------------------------------------------------------------------------------- /hyper-plugin/src/main/java/gg/amy/hyperblock/listener/PlayerOnlineListener.java: -------------------------------------------------------------------------------- 1 | package gg.amy.hyperblock.listener; 2 | 3 | import com.github.luben.zstd.Zstd; 4 | import gg.amy.hyperblock.bukkit.SkyblockChunkGenerator; 5 | import gg.amy.hyperblock.component.Database; 6 | import gg.amy.hyperblock.component.database.HyperPlayer; 7 | import gg.amy.hyperblock.utils.NbtHelpers; 8 | import gg.amy.mc.cardboard.di.Auto; 9 | import org.bukkit.Bukkit; 10 | import org.bukkit.Location; 11 | import org.bukkit.WorldCreator; 12 | import org.bukkit.event.EventHandler; 13 | import org.bukkit.event.Listener; 14 | import org.bukkit.event.player.PlayerJoinEvent; 15 | import org.bukkit.event.player.PlayerQuitEvent; 16 | 17 | import java.io.ByteArrayInputStream; 18 | import java.io.ByteArrayOutputStream; 19 | import java.io.IOException; 20 | 21 | /** 22 | * @author amy 23 | * @since 3/24/21. 24 | */ 25 | public class PlayerOnlineListener implements Listener { 26 | @Auto 27 | private Database db; 28 | 29 | @SuppressWarnings("ConstantConditions") 30 | @EventHandler 31 | public void onPlayerJoin(final PlayerJoinEvent event) { 32 | final var p = event.getPlayer(); 33 | final var world = Bukkit.createWorld(new WorldCreator(p.getUniqueId().toString()).generator(new SkyblockChunkGenerator())); 34 | // TODO: Figure out if this is even necessary 35 | world.setSpawnLocation(new Location(world, 9, 128, 9)); 36 | p.teleport(world.getSpawnLocation()); 37 | 38 | final var hp = db.getPlayerDb().get(p.getUniqueId()); 39 | // TODO: This should probably store the size of the array somewhere and 40 | // allocate only as much buffer as needed. 41 | final var buffer = new byte[4 * 1024 * 1024]; 42 | Zstd.decompress(hp.compressedInventory(), buffer); 43 | try(final var stream = new ByteArrayInputStream(buffer)) { 44 | NbtHelpers.deserializePlayerInventory(p, NbtHelpers.read(stream)); 45 | } catch(final IOException e) { 46 | throw new IllegalStateException(e); 47 | } 48 | } 49 | 50 | @EventHandler 51 | public void onPlayerLeave(final PlayerQuitEvent event) { 52 | final var uuid = event.getPlayer().getUniqueId(); 53 | final var serialized = NbtHelpers.serializePlayerInventory(event.getPlayer()); 54 | try(final var baos = new ByteArrayOutputStream()) { 55 | NbtHelpers.write(serialized, baos); 56 | final var compressed = Zstd.compress(baos.toByteArray()); 57 | final var old = db.getPlayerDb().get(uuid); 58 | final var hp = new HyperPlayer(uuid, old.snapshots(), compressed); 59 | db.getPlayerDb().set(hp); 60 | } catch(final IOException e) { 61 | throw new IllegalStateException(e); 62 | } 63 | Bukkit.unloadWorld(uuid.toString(), true); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /hyper-plugin/src/main/java/gg/amy/hyperblock/utils/NbtHelpers.java: -------------------------------------------------------------------------------- 1 | package gg.amy.hyperblock.utils; 2 | 3 | import io.netty.buffer.ByteBufInputStream; 4 | import net.minecraft.server.v1_16_R3.*; 5 | import org.bukkit.craftbukkit.v1_16_R3.inventory.CraftItemStack; 6 | import org.bukkit.entity.Player; 7 | import org.bukkit.inventory.ItemStack; 8 | import org.spigotmc.LimitStream; 9 | 10 | import javax.annotation.Nonnull; 11 | import java.io.*; 12 | import java.util.ArrayList; 13 | 14 | /** 15 | * @author amy 16 | * @since 3/23/21. 17 | */ 18 | @SuppressWarnings({"SameParameterValue", "TypeMayBeWeakened"}) 19 | public final class NbtHelpers { 20 | private NbtHelpers() { 21 | } 22 | 23 | public static NBTTagCompound serializePlayerInventory(@Nonnull final Player p) { 24 | final var inv = new NBTTagList(); 25 | final var armour = new NBTTagList(); 26 | final var extra = new NBTTagList(); 27 | for(final var i : p.getInventory().getContents()) { 28 | final var compound = new NBTTagCompound(); 29 | CraftItemStack.asNMSCopy(i).save(compound); 30 | inv.add(compound); 31 | } 32 | for(final var i : p.getInventory().getArmorContents()) { 33 | final var compound = new NBTTagCompound(); 34 | CraftItemStack.asNMSCopy(i).save(compound); 35 | armour.add(compound); 36 | } 37 | for(final var i : p.getInventory().getExtraContents()) { 38 | final var compound = new NBTTagCompound(); 39 | CraftItemStack.asNMSCopy(i).save(compound); 40 | extra.add(compound); 41 | } 42 | final var compound = new NBTTagCompound(); 43 | compound.set("inv", inv); 44 | compound.set("armour", armour); 45 | compound.set("extra", extra); 46 | return compound; 47 | } 48 | 49 | public static void deserializePlayerInventory(@Nonnull final Player p, @Nonnull final NBTTagCompound nbt) { 50 | final var inv = new ArrayList(); 51 | final var armour = new ArrayList(); 52 | final var extra = new ArrayList(); 53 | for(final var i : nbt.getList("inv", 9)) { 54 | final var bis = CraftItemStack.asBukkitCopy(net.minecraft.server.v1_16_R3.ItemStack.a((NBTTagCompound) i)); 55 | inv.add(bis); 56 | } 57 | for(final var i : nbt.getList("armour", 9)) { 58 | final var bis = CraftItemStack.asBukkitCopy(net.minecraft.server.v1_16_R3.ItemStack.a((NBTTagCompound) i)); 59 | armour.add(bis); 60 | } 61 | for(final var i : nbt.getList("extra", 9)) { 62 | final var bis = CraftItemStack.asBukkitCopy(net.minecraft.server.v1_16_R3.ItemStack.a((NBTTagCompound) i)); 63 | extra.add(bis); 64 | } 65 | p.getInventory().setContents(inv.toArray(new ItemStack[0])); 66 | p.getInventory().setArmorContents(armour.toArray(new ItemStack[0])); 67 | p.getInventory().setExtraContents(extra.toArray(new ItemStack[0])); 68 | } 69 | 70 | public static NBTTagCompound read(final InputStream inputstream) { 71 | try(final DataInputStream dis = new DataInputStream(inputstream)) { 72 | return readNbtCompoundFromInput(dis, NBTReadLimiter.a); 73 | } catch(final Exception e) { 74 | throw new IllegalStateException(e); 75 | } 76 | } 77 | 78 | public static void write(final NBTTagCompound tag, final OutputStream outputstream) { 79 | try(final DataOutputStream dis = new DataOutputStream(outputstream)) { 80 | writeTagToStream(tag, dis); 81 | } catch(final Exception e) { 82 | throw new IllegalStateException(e); 83 | } 84 | } 85 | 86 | private static void writeTagToStream(final NBTTagCompound tag, final DataOutput out) throws Exception { 87 | writeTagToDataOutput(tag, out); 88 | } 89 | 90 | private static void writeTagToDataOutput(final NBTBase nbt, final DataOutput out) throws Exception { 91 | out.writeByte(nbt.getTypeId()); 92 | if(nbt.getTypeId() != 0) { 93 | out.writeUTF(""); 94 | nbt.write(out); 95 | } 96 | } 97 | 98 | private static NBTTagCompound readNbtCompoundFromInput(DataInput di, final NBTReadLimiter readLimiter) throws Exception { 99 | if(di instanceof ByteBufInputStream) { 100 | di = new DataInputStream(new LimitStream((InputStream) di, readLimiter)); 101 | } 102 | final NBTBase nbtbase = readNextTag(di, 0, readLimiter); 103 | if(nbtbase instanceof NBTTagCompound) { 104 | return (NBTTagCompound) nbtbase; 105 | } else { 106 | throw new IOException("Root tag must be a named compound tag"); 107 | } 108 | } 109 | 110 | private static NBTBase readNextTag(final DataInput di, final int i, final NBTReadLimiter limiter) throws Exception { 111 | final byte maybeEnd = di.readByte(); 112 | if(maybeEnd == 0) { 113 | return NBTTagEnd.b; 114 | } else { 115 | di.readUTF(); 116 | return NBTTagTypes.a(maybeEnd).b(di, i, limiter); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /hyper-plugin/src/main/java/gg/amy/hyperblock/utils/ThrowingSupplier.java: -------------------------------------------------------------------------------- 1 | package gg.amy.hyperblock.utils; 2 | 3 | /** 4 | * @author amy 5 | * @since 3/22/21. 6 | */ 7 | @FunctionalInterface 8 | public interface ThrowingSupplier { 9 | T get() throws Exception; 10 | } 11 | -------------------------------------------------------------------------------- /hyper-plugin/src/main/resources/META-INF/MANIFEST.MF: -------------------------------------------------------------------------------- 1 | Agent-Class: gg.amy.hyperblock.Agent 2 | Can-Redefine-Classes: true 3 | Can-Retransform-Classes: true 4 | Can-Set-Native-Method-Prefix: true 5 | -------------------------------------------------------------------------------- /hyper-plugin/src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: "hyperblock" 2 | main: "gg.amy.hyperblock.Hyperblock" 3 | version: "${project.version}" 4 | api-version: "1.13" 5 | load: "STARTUP" 6 | -------------------------------------------------------------------------------- /libhyper/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | hyperblock 7 | gg.amy.hyperblock 8 | 0.0.1 9 | 10 | 4.0.0 11 | 12 | libhyper 13 | 14 | 15 | 16 16 | 16 17 | 18 | 19 | 20 | 21 | com.neovisionaries 22 | nv-websocket-client 23 | 2.14 24 | 25 | 26 | 27 | org.projectlombok 28 | lombok 29 | 1.18.20 30 | provided 31 | 32 | 33 | 34 | com.google.code.findbugs 35 | jsr305 36 | 3.0.2 37 | 38 | 39 | 40 | io.vertx 41 | vertx-core 42 | 3.9.4 43 | 44 | 45 | io.vertx 46 | vertx-web-client 47 | 3.9.4 48 | 49 | 50 | 51 | com.github.queer 52 | safe-vertx-completablefuture 53 | 50318b3 54 | 55 | 56 | 57 | com.google.guava 58 | guava 59 | 28.2-jre 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | src/main/resources 68 | true 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 | -------------------------------------------------------------------------------- /libhyper/src/main/java/gg/amy/hyperblock/lib/LibHyper.java: -------------------------------------------------------------------------------- 1 | package gg.amy.hyperblock.lib; 2 | 3 | /** 4 | * @author amy 5 | * @since 3/23/21. 6 | */ 7 | public final class LibHyper { 8 | private LibHyper() { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libhyper/src/main/java/gg/amy/singyeong/SingyeongClient.java: -------------------------------------------------------------------------------- 1 | package gg.amy.singyeong; 2 | 3 | import gg.amy.singyeong.client.SingyeongMessage; 4 | import gg.amy.singyeong.client.SingyeongOp; 5 | import gg.amy.singyeong.client.SingyeongSocket; 6 | import gg.amy.singyeong.client.SingyeongType; 7 | import gg.amy.singyeong.client.query.Query; 8 | import gg.amy.singyeong.data.Dispatch; 9 | import gg.amy.singyeong.data.Invalid; 10 | import gg.amy.singyeong.data.ProxiedRequest; 11 | import gg.amy.singyeong.util.JsonPojoCodec; 12 | import gg.amy.vertx.SafeVertxCompletableFuture; 13 | import io.vertx.core.Promise; 14 | import io.vertx.core.Vertx; 15 | import io.vertx.core.VertxOptions; 16 | import io.vertx.core.buffer.Buffer; 17 | import io.vertx.core.eventbus.MessageCodec; 18 | import io.vertx.core.eventbus.MessageConsumer; 19 | import io.vertx.core.json.JsonArray; 20 | import io.vertx.core.json.JsonObject; 21 | import io.vertx.ext.web.client.WebClient; 22 | import lombok.Getter; 23 | import lombok.experimental.Accessors; 24 | 25 | import javax.annotation.Nonnull; 26 | import javax.annotation.Nullable; 27 | import java.net.URI; 28 | import java.net.URISyntaxException; 29 | import java.util.*; 30 | import java.util.concurrent.CompletableFuture; 31 | import java.util.concurrent.ConcurrentHashMap; 32 | import java.util.function.Consumer; 33 | 34 | /** 35 | * @author amy 36 | * @since 10/23/18. 37 | */ 38 | @SuppressWarnings({"unused", "WeakerAccess"}) 39 | @Accessors(fluent = true) 40 | public final class SingyeongClient { 41 | public static final String SINGYEONG_DISPATCH_EVENT_CHANNEL = "singyeong:event:dispatch"; 42 | public static final String SINGYEONG_INVALID_EVENT_CHANNEL = "singyeong:event:invalid"; 43 | @Getter 44 | private final Vertx vertx; 45 | @Getter 46 | private final WebClient client; 47 | @Getter 48 | private final String serverUrl; 49 | @Getter 50 | private final String gatewayHost; 51 | @Getter 52 | private final int gatewayPort; 53 | @Getter 54 | private final boolean gatewaySsl; 55 | @Getter 56 | private final String appId; 57 | @Getter 58 | private final String authentication; 59 | @Getter 60 | private final String ip; 61 | @Getter 62 | private final Map metadataCache = new ConcurrentHashMap<>(); 63 | @Getter 64 | private final UUID id = UUID.randomUUID(); 65 | @Getter 66 | private final List tags; 67 | @Getter 68 | private SingyeongSocket socket; 69 | 70 | private SingyeongClient(@Nonnull final Vertx vertx, @Nonnull final String dsn) { 71 | this(vertx, dsn, Collections.emptyList()); 72 | } 73 | 74 | private SingyeongClient(@Nonnull final Vertx vertx, @Nonnull final String dsn, @Nonnull final List tags) { 75 | this(vertx, dsn, null, tags); 76 | } 77 | 78 | private SingyeongClient(@Nonnull final Vertx vertx, @Nonnull final String dsn, @Nullable final String ip) { 79 | this(vertx, dsn, ip, Collections.emptyList()); 80 | } 81 | 82 | private SingyeongClient(@Nonnull final Vertx vertx, @Nonnull final String dsn, @Nullable final String ip, 83 | @Nonnull final List tags) { 84 | this.vertx = vertx; 85 | client = WebClient.create(vertx); 86 | this.ip = ip; 87 | try { 88 | final var uri = new URI(dsn); 89 | String server = ""; 90 | final var scheme = uri.getScheme(); 91 | if(scheme.equalsIgnoreCase("singyeong")) { 92 | server += "ws://"; 93 | } else if(scheme.equalsIgnoreCase("ssingyeong")) { 94 | server += "wss://"; 95 | } else { 96 | throw new IllegalArgumentException(scheme + " is not a valid singyeong URI scheme (expected 'singyeong' or 'ssingyeong')"); 97 | } 98 | server += uri.getHost(); 99 | if(uri.getPort() > -1) { 100 | server += ":" + uri.getPort(); 101 | } 102 | serverUrl = server.replaceFirst("ws", "http"); 103 | gatewayHost = uri.getHost(); 104 | gatewayPort = uri.getPort() > -1 ? uri.getPort() : 80; 105 | gatewaySsl = server.startsWith("wss://"); 106 | final String userInfo = uri.getUserInfo(); 107 | if(userInfo == null) { 108 | throw new IllegalArgumentException("Didn't pass auth to singyeong DSN!"); 109 | } 110 | final var split = userInfo.split(":", 2); 111 | appId = split[0]; 112 | authentication = split.length != 2 ? null : split[1]; 113 | this.tags = Collections.unmodifiableList(tags); 114 | 115 | vertx.eventBus().registerDefaultCodec(Dispatch.class, new FakeCodec<>()); 116 | vertx.eventBus().registerDefaultCodec(Invalid.class, new FakeCodec<>()); 117 | } catch(final URISyntaxException e) { 118 | throw new IllegalArgumentException("Invalid singyeong URI!", e); 119 | } 120 | } 121 | 122 | public static SingyeongClient create(@Nonnull final String dsn) { 123 | return create(Vertx.vertx(new VertxOptions() 124 | .setWorkerPoolSize(1) 125 | .setEventLoopPoolSize(1) 126 | .setInternalBlockingPoolSize(1)), 127 | dsn); 128 | } 129 | 130 | @SuppressWarnings("WeakerAccess") 131 | public static SingyeongClient create(@Nonnull final Vertx vertx, @Nonnull final String dsn) { 132 | return new SingyeongClient(vertx, dsn); 133 | } 134 | 135 | public static SingyeongClient create(@Nonnull final String dsn, @Nonnull final List tags) { 136 | return new SingyeongClient(Vertx.vertx(new VertxOptions() 137 | .setWorkerPoolSize(2) 138 | .setEventLoopPoolSize(2) 139 | .setInternalBlockingPoolSize(2)), 140 | dsn, tags); 141 | } 142 | 143 | public static SingyeongClient create(@Nonnull final Vertx vertx, @Nonnull final String dsn, 144 | @Nonnull final List tags) { 145 | return new SingyeongClient(vertx, dsn, tags); 146 | } 147 | 148 | public static SingyeongClient create(@Nonnull final Vertx vertx, @Nonnull final String dsn, @Nullable final String ip) { 149 | return new SingyeongClient(vertx, dsn, ip); 150 | } 151 | 152 | public static SingyeongClient create(@Nonnull final Vertx vertx, @Nonnull final String dsn, @Nullable final String ip, 153 | @Nonnull final List tags) { 154 | return new SingyeongClient(vertx, dsn, ip, tags); 155 | } 156 | 157 | @Nonnull 158 | public CompletableFuture connect() { 159 | final var promise = Promise.promise(); 160 | socket = new SingyeongSocket(this); 161 | socket.connect() 162 | .thenAccept(__ -> promise.complete(null)) 163 | .exceptionally(throwable -> { 164 | promise.fail(throwable); 165 | return null; 166 | }); 167 | 168 | return SafeVertxCompletableFuture.from(vertx, promise.future()); 169 | } 170 | 171 | /** 172 | * Proxies an HTTP request to the target returned by the routing query. 173 | * 174 | * @param request The request to proxy. 175 | * 176 | * @return A future that completes with the response body when the request 177 | * is complete. 178 | */ 179 | public CompletableFuture proxy(@Nonnull final ProxiedRequest request) { 180 | final var promise = Promise.promise(); 181 | final var headers = new JsonObject(); 182 | request.headers(); 183 | request.headers().asMap().forEach((k, v) -> headers.put(k, new JsonArray(new ArrayList<>(v)))); 184 | 185 | final var query = request.query(); 186 | final var payload = new JsonObject() 187 | .put("method", request.method().name().toUpperCase()) 188 | // Assume that route is / if none specified 189 | .put("route", request.route() == null ? "/" : request.route()) 190 | .put("headers", headers) 191 | .put("body", request.body()) 192 | .put("query", new JsonObject() 193 | .put("optional", query.optional()) 194 | .put("restricted", query.restricted()) 195 | .put("application", query.target()) 196 | .put("ops", query.ops()) 197 | .put("selector", query.selector()) 198 | ); 199 | if(query.consistent()) { 200 | payload.getJsonObject("query").put("key", query.hashKey()); 201 | } 202 | 203 | client.postAbs(serverUrl + "/api/v1/proxy").putHeader("Authorization", authentication) 204 | .sendJson(payload, ar -> { 205 | if(ar.succeeded()) { 206 | final var result = ar.result(); 207 | promise.complete(result.body()); 208 | } else { 209 | promise.fail(ar.cause()); 210 | } 211 | }); 212 | 213 | return SafeVertxCompletableFuture.from(vertx, promise.future()); 214 | } 215 | 216 | /** 217 | * Handle events dispatched from the server. 218 | * 219 | * @return The consumer, in case you want to unregister it. 220 | */ 221 | public MessageConsumer onEvent(@Nonnull final Consumer consumer) { 222 | return vertx.eventBus().consumer(SINGYEONG_DISPATCH_EVENT_CHANNEL, m -> consumer.accept(m.body())); 223 | } 224 | 225 | /** 226 | * Handle messages from the server telling you that you sent a bad message. 227 | * 228 | * @return The consumer, in case you want to unregister it. 229 | */ 230 | public MessageConsumer onInvalid(@Nonnull final Consumer consumer) { 231 | return vertx.eventBus().consumer(SINGYEONG_INVALID_EVENT_CHANNEL, m -> consumer.accept(m.body())); 232 | } 233 | 234 | public void send(@Nonnull final Query query, @Nullable final T payload) { 235 | send(query, null, payload); 236 | } 237 | 238 | public void send(@Nonnull final Query query, @Nullable final String nonce, @Nullable final T payload) { 239 | send(query, nonce, false, payload); 240 | } 241 | 242 | public void send(@Nonnull final Query query, @Nullable final String nonce, final boolean optional, @Nullable final T payload) { 243 | send(query, nonce, optional, false, payload); 244 | } 245 | 246 | public void send(@Nonnull final Query query, @Nullable final String nonce, final boolean optional, 247 | final boolean droppable, @Nullable final T payload) { 248 | final var msg = createDispatch("SEND", nonce, query, optional, droppable, payload); 249 | socket.send(msg); 250 | } 251 | 252 | public void broadcast(@Nonnull final Query query, @Nullable final T payload) { 253 | send(query, null, payload); 254 | } 255 | 256 | public void broadcast(@Nonnull final Query query, @Nullable final String nonce, @Nullable final T payload) { 257 | send(query, nonce, false, payload); 258 | } 259 | 260 | public void broadcast(@Nonnull final Query query, @Nullable final String nonce, final boolean optional, @Nullable final T payload) { 261 | send(query, nonce, optional, false, payload); 262 | } 263 | 264 | public void broadcast(@Nonnull final Query query, @Nullable final String nonce, final boolean optional, 265 | final boolean droppable, @Nullable final T payload) { 266 | final var msg = createDispatch("BROADCAST", nonce, query, optional, droppable, payload); 267 | socket.send(msg); 268 | } 269 | 270 | private SingyeongMessage createDispatch(@Nonnull final String type, @Nullable final String nonce, 271 | @Nonnull final Query query, final boolean optional, final boolean droppable, 272 | @Nullable final T payload) { 273 | final var data = new JsonObject() 274 | .put("sender", id.toString()) 275 | .put("target", new JsonObject() 276 | .put("optional", query.optional()) 277 | .put("restricted", query.restricted()) 278 | .put("application", query.target()) 279 | .put("ops", query.ops()) 280 | ) 281 | .put("nonce", nonce); 282 | if(payload instanceof String || payload instanceof JsonObject || payload instanceof JsonArray) { 283 | data.put("payload", payload); 284 | } else { 285 | data.put("payload", JsonObject.mapFrom(payload)); 286 | } 287 | if(query.consistent()) { 288 | data.getJsonObject("query").put("key", query.hashKey()); 289 | } 290 | return new SingyeongMessage(SingyeongOp.DISPATCH, type, System.currentTimeMillis(), data); 291 | } 292 | 293 | /** 294 | * Update this client's metadata on the server. 295 | * 296 | * @param key The metadata key to set. 297 | * @param type The type of the metadata. Will be validated by the server. 298 | * @param data The value to set for the metadata key. 299 | * @param The Java type of the metadata. 300 | */ 301 | public void updateMetadata(@Nonnull final String key, @Nonnull final SingyeongType type, @Nonnull final T data) { 302 | final var metadataValue = new JsonObject().put("type", type.name().toLowerCase()).put("value", data); 303 | metadataCache.put(key, metadataValue); 304 | final var msg = new SingyeongMessage(SingyeongOp.DISPATCH, "UPDATE_METADATA", 305 | System.currentTimeMillis(), 306 | new JsonObject().put(key, metadataValue) 307 | ); 308 | socket.send(msg); 309 | } 310 | 311 | private void codec(@Nonnull final Class cls) { 312 | vertx.eventBus().registerDefaultCodec(cls, new JsonPojoCodec<>(cls)); 313 | } 314 | 315 | private static class FakeCodec implements MessageCodec { 316 | @Override 317 | public void encodeToWire(final Buffer buffer, final T dispatch) { 318 | } 319 | 320 | @Override 321 | public Object decodeFromWire(final int pos, final Buffer buffer) { 322 | return null; 323 | } 324 | 325 | @Override 326 | public Object transform(final T dispatch) { 327 | return dispatch; 328 | } 329 | 330 | @Override 331 | public String name() { 332 | return "noop" + new Random().nextInt(); 333 | } 334 | 335 | @Override 336 | public byte systemCodecID() { 337 | return -1; 338 | } 339 | } 340 | } 341 | -------------------------------------------------------------------------------- /libhyper/src/main/java/gg/amy/singyeong/client/SingyeongMessage.java: -------------------------------------------------------------------------------- 1 | package gg.amy.singyeong.client; 2 | 3 | import io.vertx.core.json.JsonObject; 4 | import lombok.Value; 5 | import lombok.experimental.Accessors; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import javax.annotation.Nonnull; 10 | 11 | /** 12 | * @author amy 13 | * @since 10/23/18. 14 | */ 15 | @Value 16 | @Accessors(fluent = true) 17 | public class SingyeongMessage { 18 | private static final Logger LOGGER = LoggerFactory.getLogger(SingyeongMessage.class); 19 | private SingyeongOp op; 20 | private String type; 21 | private long timestamp; 22 | JsonObject data; 23 | 24 | static SingyeongMessage fromJson(@Nonnull final JsonObject json) { 25 | final var d = json.getJsonObject("d"); 26 | LOGGER.trace("decoded payload into: {}", d); 27 | return new SingyeongMessage(SingyeongOp.fromOp(json.getInteger("op")), 28 | json.getString("t", null), json.getLong("ts"), d); 29 | } 30 | 31 | JsonObject toJson() { 32 | return new JsonObject() 33 | .put("op", op.code()) 34 | .put("t", type) 35 | .put("ts", timestamp) 36 | .put("d", data) 37 | ; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /libhyper/src/main/java/gg/amy/singyeong/client/SingyeongOp.java: -------------------------------------------------------------------------------- 1 | package gg.amy.singyeong.client; 2 | 3 | import lombok.Getter; 4 | import lombok.experimental.Accessors; 5 | 6 | import javax.annotation.Nonnegative; 7 | 8 | /** 9 | * @author amy 10 | * @since 10/23/18. 11 | */ 12 | @Accessors(fluent = true) 13 | public enum SingyeongOp { 14 | HELLO(0), 15 | IDENTIFY(1), 16 | READY(2), 17 | INVALID(3), 18 | DISPATCH(4), 19 | HEARTBEAT(5), 20 | HEARTBEAT_ACK(6), 21 | ; 22 | @Getter 23 | private final int code; 24 | 25 | SingyeongOp(final int code) { 26 | this.code = code; 27 | } 28 | 29 | public static SingyeongOp fromOp(@Nonnegative final int op) { 30 | for(final var value : values()) { 31 | if(value.code == op) { 32 | return value; 33 | } 34 | } 35 | throw new IllegalArgumentException(op + " is not a valid opcode"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /libhyper/src/main/java/gg/amy/singyeong/client/SingyeongSocket.java: -------------------------------------------------------------------------------- 1 | package gg.amy.singyeong.client; 2 | 3 | import gg.amy.singyeong.SingyeongClient; 4 | import gg.amy.singyeong.data.Dispatch; 5 | import gg.amy.singyeong.data.Invalid; 6 | import gg.amy.vertx.SafeVertxCompletableFuture; 7 | import io.vertx.core.Promise; 8 | import io.vertx.core.http.*; 9 | import io.vertx.core.json.JsonObject; 10 | import lombok.AccessLevel; 11 | import lombok.Getter; 12 | import lombok.RequiredArgsConstructor; 13 | import lombok.experimental.Accessors; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import javax.annotation.Nonnegative; 18 | import javax.annotation.Nonnull; 19 | import java.net.Inet4Address; 20 | import java.net.UnknownHostException; 21 | import java.util.concurrent.CompletableFuture; 22 | import java.util.concurrent.atomic.AtomicReference; 23 | 24 | /** 25 | * @author amy 26 | * @since 10/23/18. 27 | */ 28 | @RequiredArgsConstructor 29 | @Accessors(fluent = true) 30 | public final class SingyeongSocket { 31 | private final SingyeongClient singyeong; 32 | private final AtomicReference socketRef = new AtomicReference<>(null); 33 | private final Logger logger = LoggerFactory.getLogger(getClass()); 34 | @Getter(AccessLevel.PACKAGE) 35 | private HttpClient client; 36 | private long heartbeatTask; 37 | 38 | @Nonnull 39 | public CompletableFuture connect() { 40 | final var promise = Promise.promise(); 41 | 42 | client = singyeong.vertx().createHttpClient(new HttpClientOptions() 43 | .setMaxWebSocketFrameSize(Integer.MAX_VALUE) 44 | .setMaxWebSocketMessageSize(Integer.MAX_VALUE)); 45 | promise.future().onComplete(res -> { 46 | if(res.failed()) { 47 | handleClose(null); 48 | } 49 | }); 50 | connectLoop(promise); 51 | 52 | return SafeVertxCompletableFuture.from(singyeong.vertx(), promise.future()); 53 | } 54 | 55 | private void connectLoop(final Promise promise) { 56 | logger.info("Starting Singyeong connect..."); 57 | final WebSocketConnectOptions opts = new WebSocketConnectOptions() 58 | .setHost(singyeong.gatewayHost()) 59 | .setPort(singyeong.gatewayPort()) 60 | .setSsl(singyeong.gatewaySsl()) 61 | .setURI("/gateway/websocket"); 62 | client.webSocket(opts, res -> { 63 | if(res.succeeded()) { 64 | handleSocketConnect(res.result()); 65 | promise.complete(null); 66 | } else { 67 | final var e = res.cause(); 68 | e.printStackTrace(); 69 | singyeong.vertx().setTimer(1_000L, __ -> connectLoop(promise)); 70 | } 71 | }); 72 | } 73 | 74 | private void handleSocketConnect(@Nonnull final WebSocket socket) { 75 | socket.frameHandler(this::handleFrame); 76 | socket.closeHandler(this::handleClose); 77 | socketRef.set(socket); 78 | logger.info("Connected to Singyeong!"); 79 | } 80 | 81 | @SuppressWarnings("unused") 82 | private void handleClose(final Void __) { 83 | logger.warn("Disconnected from Singyeong!"); 84 | socketRef.set(null); 85 | singyeong.vertx().cancelTimer(heartbeatTask); 86 | singyeong.vertx().setTimer(1_000L, ___ -> connectLoop(Promise.promise())); 87 | } 88 | 89 | private void handleFrame(@Nonnull final WebSocketFrame frame) { 90 | if(frame.isText()) { 91 | final var payload = new JsonObject(frame.textData()); 92 | logger.trace("Received new JSON payload: {}", payload); 93 | final var msg = SingyeongMessage.fromJson(payload); 94 | logger.trace("op = {}", msg.op().name()); 95 | switch(msg.op()) { 96 | case HELLO: { 97 | final var heartbeatInterval = msg.data().getInteger("heartbeat_interval"); 98 | // IDENTIFY to allow doing everything 99 | send(identify()); 100 | startHeartbeat(heartbeatInterval); 101 | break; 102 | } 103 | case READY: { 104 | // Welcome to singyeong! 105 | logger.info("Welcome to singyeong!"); 106 | if(!singyeong.metadataCache().isEmpty()) { 107 | logger.info("Refreshing metadata (we probably just reconnected)"); 108 | final var data = new JsonObject(); 109 | singyeong.metadataCache().forEach(data::put); 110 | final var update = new SingyeongMessage(SingyeongOp.DISPATCH, "UPDATE_METADATA", 111 | System.currentTimeMillis(), 112 | data 113 | ); 114 | send(update); 115 | } 116 | break; 117 | } 118 | case INVALID: { 119 | final var error = msg.data().getString("error"); 120 | singyeong.vertx().eventBus().publish(SingyeongClient.SINGYEONG_INVALID_EVENT_CHANNEL, 121 | new Invalid(error, msg.data() 122 | // lol 123 | .getJsonObject("d", new JsonObject().put("nonce", (String) null)) 124 | .getString("nonce"))); 125 | socketRef.get().close(); 126 | break; 127 | } 128 | case DISPATCH: { 129 | logger.trace("sending dispatch"); 130 | final var d = msg.data(); 131 | singyeong.vertx().eventBus().publish(SingyeongClient.SINGYEONG_DISPATCH_EVENT_CHANNEL, 132 | new Dispatch(msg.timestamp(), d.getString("sender"), d.getString("nonce"), 133 | d.getValue("payload"))); 134 | break; 135 | } 136 | case HEARTBEAT_ACK: { 137 | // Avoid disconnection for another day~ 138 | break; 139 | } 140 | default: { 141 | logger.warn("Got unknown singyeong opcode " + msg.op()); 142 | break; 143 | } 144 | } 145 | } 146 | } 147 | 148 | public void send(@Nonnull final SingyeongMessage msg) { 149 | if(socketRef.get() != null) { 150 | socketRef.get().writeTextMessage(msg.toJson().encode()); 151 | logger.debug("Sending singyeong payload:\n{}", msg.toJson().encodePrettily()); 152 | } 153 | } 154 | 155 | private void startHeartbeat(@Nonnegative final int heartbeatInterval) { 156 | // Delay a second before starting just to be safe wrt IDENTIFY 157 | singyeong.vertx().setTimer(1_000L, __ -> { 158 | send(heartbeat()); 159 | singyeong.vertx().setPeriodic(heartbeatInterval, id -> { 160 | heartbeatTask = id; 161 | if(socketRef.get() != null) { 162 | send(heartbeat()); 163 | } else { 164 | singyeong.vertx().cancelTimer(heartbeatTask); 165 | } 166 | }); 167 | }); 168 | } 169 | 170 | private SingyeongMessage identify() { 171 | final var payload = new JsonObject() 172 | .put("client_id", singyeong.id().toString()) 173 | .put("application_id", singyeong.appId()); 174 | if(singyeong.authentication() != null) { 175 | payload.put("auth", singyeong.authentication()); 176 | } 177 | payload.put("ip", ip()); 178 | return new SingyeongMessage(SingyeongOp.IDENTIFY, null, System.currentTimeMillis(), payload); 179 | } 180 | 181 | @Nonnull 182 | private String ip() { 183 | if(singyeong.ip() != null && !singyeong.ip().isEmpty()) { 184 | return singyeong.ip(); 185 | } 186 | // Attempt to support kube users who put it as an env var 187 | final var podIpEnv = System.getenv("POD_IP"); 188 | if(podIpEnv != null) { 189 | return podIpEnv; 190 | } 191 | try { 192 | return Inet4Address.getLocalHost().getHostAddress(); 193 | } catch(final UnknownHostException e) { 194 | throw new IllegalStateException("DNS broken? Can't resolve localhost!", e); 195 | } 196 | } 197 | 198 | private SingyeongMessage heartbeat() { 199 | return new SingyeongMessage(SingyeongOp.HEARTBEAT, null, System.currentTimeMillis(), 200 | new JsonObject() 201 | .put("client_id", singyeong.id().toString()) 202 | ); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /libhyper/src/main/java/gg/amy/singyeong/client/SingyeongType.java: -------------------------------------------------------------------------------- 1 | package gg.amy.singyeong.client; 2 | 3 | /** 4 | * @author amy 5 | * @since 10/24/18. 6 | */ 7 | @SuppressWarnings("unused") 8 | public enum SingyeongType { 9 | STRING, 10 | INTEGER, 11 | FLOAT, 12 | VERSION, 13 | LIST, 14 | } 15 | -------------------------------------------------------------------------------- /libhyper/src/main/java/gg/amy/singyeong/client/query/Query.java: -------------------------------------------------------------------------------- 1 | package gg.amy.singyeong.client.query; 2 | 3 | import io.vertx.core.json.JsonArray; 4 | import io.vertx.core.json.JsonObject; 5 | import lombok.AccessLevel; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import lombok.experimental.Accessors; 9 | 10 | import java.util.Collection; 11 | 12 | /** 13 | * @author amy 14 | * @since 6/9/19. 15 | */ 16 | @Getter 17 | @Accessors(fluent = true) 18 | @AllArgsConstructor(access = AccessLevel.PACKAGE) 19 | public final class Query { 20 | private final String target; 21 | private final JsonArray ops; 22 | private final boolean optional; 23 | private final boolean restricted; 24 | private final boolean consistent; 25 | private final String hashKey; 26 | private final JsonObject selector; 27 | } 28 | -------------------------------------------------------------------------------- /libhyper/src/main/java/gg/amy/singyeong/client/query/QueryBuilder.java: -------------------------------------------------------------------------------- 1 | package gg.amy.singyeong.client.query; 2 | 3 | import io.vertx.core.json.JsonArray; 4 | import io.vertx.core.json.JsonObject; 5 | 6 | import javax.annotation.Nonnull; 7 | import javax.annotation.Nullable; 8 | import java.util.ArrayList; 9 | import java.util.Collection; 10 | import java.util.List; 11 | 12 | /** 13 | * @author amy 14 | * @since 10/23/18. 15 | */ 16 | @SuppressWarnings("unused") 17 | public final class QueryBuilder { 18 | private final Collection ops = new ArrayList<>(); 19 | private boolean optional; 20 | private boolean restricted; 21 | private String hashKey; 22 | private String target; 23 | private JsonObject selector; 24 | 25 | public QueryBuilder eq(@Nonnull final String key, @Nullable final T value) { 26 | ops.add(new JsonObject().put("path", key).put("op", "$eq").put("to", new JsonObject().put("value", value))); 27 | return this; 28 | } 29 | 30 | public QueryBuilder ne(@Nonnull final String key, @Nullable final T value) { 31 | ops.add(new JsonObject().put("path", key).put("op", "$ne").put("to", new JsonObject().put("value", value))); 32 | return this; 33 | } 34 | 35 | public QueryBuilder gt(@Nonnull final String key, @Nullable final T value) { 36 | ops.add(new JsonObject().put("path", key).put("op", "$gt").put("to", new JsonObject().put("value", value))); 37 | return this; 38 | } 39 | 40 | public QueryBuilder gte(@Nonnull final String key, @Nullable final T value) { 41 | ops.add(new JsonObject().put("path", key).put("op", "$gte").put("to", new JsonObject().put("value", value))); 42 | return this; 43 | } 44 | 45 | public QueryBuilder lt(@Nonnull final String key, @Nullable final T value) { 46 | ops.add(new JsonObject().put("path", key).put("op", "$lt").put("to", new JsonObject().put("value", value))); 47 | return this; 48 | } 49 | 50 | public QueryBuilder lte(@Nonnull final String key, @Nullable final T value) { 51 | ops.add(new JsonObject().put("path", key).put("op", "$lte").put("to", new JsonObject().put("value", value))); 52 | return this; 53 | } 54 | 55 | public QueryBuilder in(@Nonnull final String key, @Nullable final T value) { 56 | ops.add(new JsonObject().put("path", key).put("op", "$in").put("to", new JsonObject().put("value", value))); 57 | return this; 58 | } 59 | 60 | public QueryBuilder nin(@Nonnull final String key, @Nullable final T value) { 61 | ops.add(new JsonObject().put("path", key).put("op", "$nin").put("to", new JsonObject().put("value", value))); 62 | return this; 63 | } 64 | 65 | public QueryBuilder contains(@Nonnull final String key, @Nullable final T value) { 66 | ops.add(new JsonObject().put("path", key).put("op", "$contains").put("to", new JsonObject().put("value", value))); 67 | return this; 68 | } 69 | 70 | public QueryBuilder ncontains(@Nonnull final String key, @Nullable final T value) { 71 | ops.add(new JsonObject().put("path", key).put("op", "$ncontains").put("to", new JsonObject().put("value", value))); 72 | return this; 73 | } 74 | 75 | public QueryBuilder and(@Nonnull final QueryBuilder value) { 76 | if(value.ops.isEmpty()) { 77 | throw new IllegalArgumentException("Passed QueryBuilder doesn't have any ops!"); 78 | } 79 | ops.add(new JsonObject().put("op", "$and").put("with", value.ops)); 80 | return this; 81 | } 82 | 83 | public QueryBuilder or(@Nonnull final QueryBuilder value) { 84 | if(value.ops.isEmpty()) { 85 | throw new IllegalArgumentException("Passed QueryBuilder doesn't have any ops!"); 86 | } 87 | ops.add(new JsonObject().put("op", "$or").put("with", value.ops)); 88 | return this; 89 | } 90 | 91 | public QueryBuilder nor(@Nonnull final QueryBuilder value) { 92 | if(value.ops.isEmpty()) { 93 | throw new IllegalArgumentException("Passed QueryBuilder doesn't have any ops!"); 94 | } 95 | ops.add(new JsonObject().put("op", "$nor").put("with", value.ops)); 96 | return this; 97 | } 98 | 99 | public QueryBuilder selectMin(@Nonnull final String key) { 100 | selector = new JsonObject().put("$min", key); 101 | return this; 102 | } 103 | 104 | public QueryBuilder selectMax(@Nonnull final String key) { 105 | selector = new JsonObject().put("$max", key); 106 | return this; 107 | } 108 | 109 | public QueryBuilder selectAvg(@Nonnull final String key) { 110 | selector = new JsonObject().put("$avg", key); 111 | return this; 112 | } 113 | 114 | /** 115 | * Directly adds the specified ops to the list of ops. Input is not 116 | * validated. 117 | * 118 | * @param ops The ops to add. 119 | * @return Itself. 120 | */ 121 | public QueryBuilder withOps(@Nonnull final Collection ops) { 122 | this.ops.addAll(ops); 123 | return this; 124 | } 125 | 126 | public QueryBuilder optional(final boolean optional) { 127 | this.optional = optional; 128 | return this; 129 | } 130 | 131 | public QueryBuilder restricted(final boolean restricted) { 132 | this.restricted = restricted; 133 | return this; 134 | } 135 | 136 | public QueryBuilder target(@Nullable final String target) { 137 | this.target = target; 138 | return this; 139 | } 140 | 141 | public QueryBuilder hashKey(@Nullable final String hashKey) { 142 | this.hashKey = hashKey; 143 | return this; 144 | } 145 | 146 | public Query build() { 147 | return new Query(target, new JsonArray(List.copyOf(ops)), optional, restricted, hashKey != null, hashKey, selector); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /libhyper/src/main/java/gg/amy/singyeong/data/Dispatch.java: -------------------------------------------------------------------------------- 1 | package gg.amy.singyeong.data; 2 | 3 | import lombok.Value; 4 | import lombok.experimental.Accessors; 5 | 6 | /** 7 | * @author amy 8 | * @since 10/23/18. 9 | */ 10 | @Value 11 | @Accessors(fluent = true) 12 | public class Dispatch { 13 | long timestamp; 14 | String sender; 15 | String nonce; 16 | Object data; 17 | } 18 | -------------------------------------------------------------------------------- /libhyper/src/main/java/gg/amy/singyeong/data/Invalid.java: -------------------------------------------------------------------------------- 1 | package gg.amy.singyeong.data; 2 | 3 | import lombok.Value; 4 | import lombok.experimental.Accessors; 5 | 6 | /** 7 | * @author amy 8 | * @since 10/23/18. 9 | */ 10 | @Value 11 | @Accessors(fluent = true) 12 | public class Invalid { 13 | private String reason; 14 | private String nonce; 15 | } 16 | -------------------------------------------------------------------------------- /libhyper/src/main/java/gg/amy/singyeong/data/ProxiedRequest.java: -------------------------------------------------------------------------------- 1 | package gg.amy.singyeong.data; 2 | 3 | import com.google.common.collect.Multimap; 4 | import com.google.common.collect.Multimaps; 5 | import gg.amy.singyeong.client.query.Query; 6 | import io.vertx.core.http.HttpMethod; 7 | import lombok.Builder; 8 | import lombok.Builder.Default; 9 | import lombok.Value; 10 | import lombok.experimental.Accessors; 11 | 12 | import java.util.ArrayList; 13 | import java.util.HashMap; 14 | 15 | /** 16 | * A proxied request that singyeong can execute on behalf of the client. 17 | * 18 | * @author amy 19 | * @since 6/5/19. 20 | */ 21 | @Value 22 | @Accessors(fluent = true) 23 | @Builder(toBuilder = true) 24 | public final class ProxiedRequest { 25 | @Default 26 | private final HttpMethod method = HttpMethod.GET; 27 | private final String route; 28 | private final Query query; 29 | @Default 30 | private final Multimap headers = Multimaps.newMultimap(new HashMap<>(), ArrayList::new); 31 | private final Object body; 32 | } 33 | -------------------------------------------------------------------------------- /libhyper/src/main/java/gg/amy/singyeong/util/JsonPojoCodec.java: -------------------------------------------------------------------------------- 1 | package gg.amy.singyeong.util; 2 | 3 | import io.vertx.core.buffer.Buffer; 4 | import io.vertx.core.eventbus.MessageCodec; 5 | import io.vertx.core.json.JsonObject; 6 | 7 | /** 8 | * @author amy 9 | * @since 10/24/18. 10 | */ 11 | public class JsonPojoCodec implements MessageCodec { 12 | private final Class type; 13 | 14 | public JsonPojoCodec(final Class type) { 15 | this.type = type; 16 | } 17 | 18 | @Override 19 | public void encodeToWire(final Buffer buffer, final T t) { 20 | buffer.appendString(JsonObject.mapFrom(t).encode()); 21 | } 22 | 23 | @Override 24 | public T decodeFromWire(final int pos, final Buffer buffer) { 25 | final var out = Buffer.buffer(); 26 | buffer.readFromBuffer(pos, out); 27 | final var data = new JsonObject(out.getString(0, out.length())); 28 | return data.mapTo(type); 29 | } 30 | 31 | @Override 32 | public T transform(final T t) { 33 | return t; 34 | } 35 | 36 | @Override 37 | public String name() { 38 | return "JsonPojoCodec-" + type.getName(); 39 | } 40 | 41 | @Override 42 | public byte systemCodecID() { 43 | return -1; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | gg.amy.hyperblock 8 | hyperblock 9 | 0.0.1 10 | pom 11 | 12 | 13 | 16 14 | 16 15 | 16 | 17 | 18 | 19 | jitpack 20 | jitpack 21 | https://jitpack.io/ 22 | default 23 | 24 | 25 | 26 | 27 | hyper-plugin 28 | libhyper 29 | 30 | 31 | --------------------------------------------------------------------------------