From b7dc453879684ee7957fa8a42cbcde215862c1df Mon Sep 17 00:00:00 2001 From: Mike Primm Date: Sat, 16 May 2020 13:29:39 -0500 Subject: [PATCH] Drop unsupported versions (1.13.0, 1.13.1, 1.14.0) --- .../src/main/java/org/dynmap/DynmapCore.java | 15 +- forge-1.14.4/.gitignore | 3 + forge-1.14.4/build.gradle | 89 + .../dynmap/forge_1_14_4/ChunkSnapshot.java | 263 +++ .../org/dynmap/forge_1_14_4/ClientProxy.java | 6 + .../org/dynmap/forge_1_14_4/DynmapMod.java | 127 + .../org/dynmap/forge_1_14_4/DynmapPlugin.java | 2093 +++++++++++++++++ .../forge_1_14_4/ForgeMapChunkCache.java | 1603 +++++++++++++ .../org/dynmap/forge_1_14_4/ForgeWorld.java | 261 ++ .../java/org/dynmap/forge_1_14_4/Proxy.java | 24 + .../dynmap/forge_1_14_4/SnapshotCache.java | 191 ++ .../org/dynmap/forge_1_14_4/VersionCheck.java | 97 + .../permissions/FilePermissions.java | 103 + .../permissions/OpPermissions.java | 51 + .../permissions/PermissionProvider.java | 15 + .../src/main/resources/META-INF/mods.toml | 25 + .../src/main/resources/configuration.txt | 443 ++++ forge-1.14.4/src/main/resources/dynmap_at.cfg | 4 + forge-1.14.4/src/main/resources/pack.mcmeta | 6 + .../main/resources/permissions.yml.example | 27 + gradle.properties | 1 + gradle/wrapper/gradle-wrapper.jar | Bin 54413 -> 54708 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle | 2 + .../main/java/org/dynmap/bukkit/Helper.java | 14 +- 25 files changed, 5450 insertions(+), 15 deletions(-) create mode 100644 forge-1.14.4/.gitignore create mode 100644 forge-1.14.4/build.gradle create mode 100644 forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/ChunkSnapshot.java create mode 100644 forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/ClientProxy.java create mode 100644 forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/DynmapMod.java create mode 100644 forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/DynmapPlugin.java create mode 100644 forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/ForgeMapChunkCache.java create mode 100644 forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/ForgeWorld.java create mode 100644 forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/Proxy.java create mode 100644 forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/SnapshotCache.java create mode 100644 forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/VersionCheck.java create mode 100644 forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/permissions/FilePermissions.java create mode 100644 forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/permissions/OpPermissions.java create mode 100644 forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/permissions/PermissionProvider.java create mode 100644 forge-1.14.4/src/main/resources/META-INF/mods.toml create mode 100644 forge-1.14.4/src/main/resources/configuration.txt create mode 100644 forge-1.14.4/src/main/resources/dynmap_at.cfg create mode 100644 forge-1.14.4/src/main/resources/pack.mcmeta create mode 100644 forge-1.14.4/src/main/resources/permissions.yml.example diff --git a/DynmapCore/src/main/java/org/dynmap/DynmapCore.java b/DynmapCore/src/main/java/org/dynmap/DynmapCore.java index d68ee61c..c8530e14 100644 --- a/DynmapCore/src/main/java/org/dynmap/DynmapCore.java +++ b/DynmapCore/src/main/java/org/dynmap/DynmapCore.java @@ -564,7 +564,20 @@ public class DynmapCore implements DynmapCommonAPI { //dumpColorMap("dokuhigh.txt", "dokuhigh.zip"); //dumpColorMap("misa.txt", "misa.zip"); //dumpColorMap("sphax.txt", "sphax.zip"); - + + Log.info("Block Name dump"); + Log.info("---------------"); + for (int i = 0; i < DynmapBlockState.getGlobalIndexMax(); ) { + DynmapBlockState bs = DynmapBlockState.getStateByGlobalIndex(i); + if (bs != null) { + Log.info(String.format("%d,%s,%d", i, bs.blockName, bs.getStateCount())); + i += bs.getStateCount(); + } + else { + i++; + } + } + Log.info("---------------"); return true; } diff --git a/forge-1.14.4/.gitignore b/forge-1.14.4/.gitignore new file mode 100644 index 00000000..54603950 --- /dev/null +++ b/forge-1.14.4/.gitignore @@ -0,0 +1,3 @@ +/build/ +/.gradle/ +/bin/ diff --git a/forge-1.14.4/build.gradle b/forge-1.14.4/build.gradle new file mode 100644 index 00000000..0b5eb6bd --- /dev/null +++ b/forge-1.14.4/build.gradle @@ -0,0 +1,89 @@ +buildscript { + repositories { + maven { url = 'https://files.minecraftforge.net/maven' } + jcenter() + mavenCentral() + } + dependencies { + classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '3.+', changing: true + } +} +apply plugin: 'net.minecraftforge.gradle' +apply plugin: 'eclipse' +apply plugin: 'com.github.johnrengelman.shadow' + +sourceCompatibility = targetCompatibility = compileJava.sourceCompatibility = compileJava.targetCompatibility = '1.8' // Need this here so eclipse task generates correctly. + +ext.buildNumber = System.getenv().BUILD_NUMBER ?: "Dev" + +minecraft { + mappings channel: 'snapshot', version: '20190719-1.14.3' + runs { + server { + workingDirectory project.file('run').canonicalPath + } + } +} + +project.archivesBaseName = "${project.archivesBaseName}-forge-1.14.4" + +dependencies { + compile project(path: ":DynmapCore", configuration: "shadow") + minecraft 'net.minecraftforge:forge:1.14.4-28.2.10' +} + +processResources +{ + inputs.property "version", project.version + '-' + project.ext.buildNumber + + // replace stuff in mcmod.info, nothing else + from(sourceSets.main.resources.srcDirs) { + include 'META-INF/mods.toml' + + // replace version and mcversion + expand( + version: project.version + '-' + project.ext.buildNumber, + mcversion: "1.14.4" + ) + } + + // copy everything else, thats not the mcmod.info + from(sourceSets.main.resources.srcDirs) { + exclude 'META-INF/mods.toml' + } +} + +shadowJar { + dependencies { + include(dependency(':DynmapCore')) + } + archiveName = "Dynmap-${parent.version}-forge-1.14.4.jar" + destinationDir = file '../target' + manifest { + attributes 'FMLAT': 'dynmap_at.cfg' + } +} + +shadowJar.doLast { + task -> + ant.checksum file: task.archivePath +} + +afterEvaluate { +reobf { + shadowJar { + mappings = createMcpToSrg.output + } +} +} + +task deobfJar(type: Jar) { + from sourceSets.main.output + classifier = 'dev' +} + +artifacts { + archives deobfJar +} + +build.dependsOn(shadowJar) diff --git a/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/ChunkSnapshot.java b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/ChunkSnapshot.java new file mode 100644 index 00000000..d07a7a7f --- /dev/null +++ b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/ChunkSnapshot.java @@ -0,0 +1,263 @@ +package org.dynmap.forge_1_14_4; + +import java.util.Arrays; + +import org.dynmap.Log; +import org.dynmap.renderer.DynmapBlockState; + +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.ListNBT; +import net.minecraft.util.BitArray; + +/** + * Represents a static, thread-safe snapshot of chunk of blocks + * Purpose is to allow clean, efficient copy of a chunk data to be made, and then handed off for processing in another thread (e.g. map rendering) + */ +public class ChunkSnapshot +{ + private static interface Section { + public DynmapBlockState getBlockType(int x, int y, int z); + public int getBlockSkyLight(int x, int y, int z); + public int getBlockEmittedLight(int x, int y, int z); + public boolean isEmpty(); + } + + private final int x, z; + private final Section[] section; + private final int[] hmap; // Height map + private final int[] biome; + private final long captureFulltime; + private final int sectionCnt; + private final long inhabitedTicks; + + private static final int BLOCKS_PER_SECTION = 16 * 16 * 16; + private static final int COLUMNS_PER_CHUNK = 16 * 16; + private static final byte[] emptyData = new byte[BLOCKS_PER_SECTION / 2]; + private static final byte[] fullData = new byte[BLOCKS_PER_SECTION / 2]; + + static + { + Arrays.fill(fullData, (byte)0xFF); + } + + private static class EmptySection implements Section { + @Override + public DynmapBlockState getBlockType(int x, int y, int z) { + return DynmapBlockState.AIR; + } + @Override + public int getBlockSkyLight(int x, int y, int z) { + return 15; + } + @Override + public int getBlockEmittedLight(int x, int y, int z) { + return 0; + } + @Override + public boolean isEmpty() { + return true; + } + } + + private static final EmptySection empty_section = new EmptySection(); + + private static class StdSection implements Section { + DynmapBlockState[] states; + byte[] skylight; + byte[] emitlight; + + public StdSection() { + states = new DynmapBlockState[BLOCKS_PER_SECTION]; + Arrays.fill(states, DynmapBlockState.AIR); + skylight = emptyData; + emitlight = emptyData; + } + @Override + public DynmapBlockState getBlockType(int x, int y, int z) { + return states[((y & 0xF) << 8) | (z << 4) | x]; + } + @Override + public int getBlockSkyLight(int x, int y, int z) { + int off = ((y & 0xF) << 7) | (z << 3) | (x >> 1); + return (skylight[off] >> (4 * (x & 1))) & 0xF; + } + @Override + public int getBlockEmittedLight(int x, int y, int z) + { + int off = ((y & 0xF) << 7) | (z << 3) | (x >> 1); + return (emitlight[off] >> (4 * (x & 1))) & 0xF; + } + @Override + public boolean isEmpty() { + return false; + } + } + /** + * Construct empty chunk snapshot + * + * @param x + * @param z + */ + public ChunkSnapshot(int worldheight, int x, int z, long captime, long inhabitedTime) + { + this.x = x; + this.z = z; + this.captureFulltime = captime; + this.biome = new int[COLUMNS_PER_CHUNK]; + this.sectionCnt = worldheight / 16; + /* Allocate arrays indexed by section */ + this.section = new Section[this.sectionCnt]; + + /* Fill with empty data */ + for (int i = 0; i < this.sectionCnt; i++) { + this.section[i] = empty_section; + } + + /* Create empty height map */ + this.hmap = new int[16 * 16]; + + this.inhabitedTicks = inhabitedTime; + } + + public ChunkSnapshot(CompoundNBT nbt, int worldheight) { + this.x = nbt.getInt("xPos"); + this.z = nbt.getInt("zPos"); + this.captureFulltime = 0; + this.hmap = nbt.getIntArray("HeightMap"); + this.sectionCnt = worldheight / 16; + if (nbt.contains("InhabitedTime")) { + this.inhabitedTicks = nbt.getLong("InhabitedTime"); + } + else { + this.inhabitedTicks = 0; + } + /* Allocate arrays indexed by section */ + this.section = new Section[this.sectionCnt]; + /* Fill with empty data */ + for (int i = 0; i < this.sectionCnt; i++) { + this.section[i] = empty_section; + } + /* Get sections */ + ListNBT sect = nbt.getList("Sections", 10); + for (int i = 0; i < sect.size(); i++) { + CompoundNBT sec = sect.getCompound(i); + int secnum = sec.getByte("Y"); + if (secnum >= this.sectionCnt) { + //Log.info("Section " + (int) secnum + " above world height " + worldheight); + continue; + } + if (secnum < 0) + continue; + //System.out.println("section(" + secnum + ")=" + sec.asString()); + // Create normal section to initialize + StdSection cursect = new StdSection(); + this.section[secnum] = cursect; + DynmapBlockState[] states = cursect.states; + DynmapBlockState[] palette = null; + // If we've got palette and block states list, process non-empty section + if (sec.contains("Palette", 9) && sec.contains("BlockStates", 12)) { + ListNBT plist = sec.getList("Palette", 10); + long[] statelist = sec.getLongArray("BlockStates"); + palette = new DynmapBlockState[plist.size()]; + for (int pi = 0; pi < plist.size(); pi++) { + CompoundNBT tc = plist.getCompound(pi); + String pname = tc.getString("Name"); + if (tc.contains("Properties")) { + StringBuilder statestr = new StringBuilder(); + CompoundNBT prop = tc.getCompound("Properties"); + for (String pid : prop.keySet()) { + if (statestr.length() > 0) statestr.append(','); + statestr.append(pid).append('=').append(prop.get(pid).getString()); + } + palette[pi] = DynmapBlockState.getStateByNameAndState(pname, statestr.toString()); + } + if (palette[pi] == null) { + palette[pi] = DynmapBlockState.getBaseStateByName(pname); + } + if (palette[pi] == null) { + palette[pi] = DynmapBlockState.AIR; + } + } + int bitsperblock = (statelist.length * 64) / 4096; + BitArray db = new BitArray(bitsperblock, 4096, statelist); + if (bitsperblock > 8) { // Not palette + for (int j = 0; j < 4096; j++) { + states[j] = DynmapBlockState.getStateByGlobalIndex(db.getAt(j)); + } + } + else { + for (int j = 0; j < 4096; j++) { + int v = db.getAt(j); + states[j] = (v < palette.length) ? palette[v] : DynmapBlockState.AIR; + } + } + } + if (sec.contains("BlockLight")) { + cursect.emitlight = sec.getByteArray("BlockLight"); + } + if (sec.contains("SkyLight")) { + cursect.skylight = sec.getByteArray("SkyLight"); + } + } + /* Get biome data */ + this.biome = new int[COLUMNS_PER_CHUNK]; + if (nbt.contains("Biomes")) { + int[] bb = nbt.getIntArray("Biomes"); + if (bb != null) { + for (int i = 0; i < bb.length; i++) { + int bv = bb[i]; + this.biome[i] = (bv < 0) ? 0 : bv; + } + } + } + } + + public int getX() + { + return x; + } + + public int getZ() + { + return z; + } + + public DynmapBlockState getBlockType(int x, int y, int z) + { + return section[y >> 4].getBlockType(x, y, z); + } + + public int getBlockSkyLight(int x, int y, int z) + { + return section[y >> 4].getBlockSkyLight(x, y, z); + } + + public int getBlockEmittedLight(int x, int y, int z) + { + return section[y >> 4].getBlockEmittedLight(x, y, z); + } + + public int getHighestBlockYAt(int x, int z) + { + return hmap[z << 4 | x]; + } + + public int getBiome(int x, int z) + { + return biome[z << 4 | x]; + } + + public final long getCaptureFullTime() + { + return captureFulltime; + } + + public boolean isSectionEmpty(int sy) + { + return section[sy].isEmpty(); + } + + public long getInhabitedTicks() { + return inhabitedTicks; + } +} diff --git a/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/ClientProxy.java b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/ClientProxy.java new file mode 100644 index 00000000..be8fedb9 --- /dev/null +++ b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/ClientProxy.java @@ -0,0 +1,6 @@ +package org.dynmap.forge_1_14_4; + +public class ClientProxy extends Proxy { + public ClientProxy() { + } +} diff --git a/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/DynmapMod.java b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/DynmapMod.java new file mode 100644 index 00000000..63c5eb7c --- /dev/null +++ b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/DynmapMod.java @@ -0,0 +1,127 @@ +package org.dynmap.forge_1_14_4; + +import java.io.File; + +import org.dynmap.DynmapCommonAPI; +import org.dynmap.DynmapCommonAPIListener; +import org.dynmap.Log; +import org.dynmap.forge_1_14_4.DynmapPlugin.OurLog; + +import net.minecraft.server.MinecraftServer; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.DistExecutor; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; +import net.minecraftforge.fml.event.lifecycle.FMLLoadCompleteEvent; +import net.minecraftforge.fml.event.server.FMLServerStartingEvent; +import net.minecraftforge.fml.event.server.FMLServerStoppingEvent; +import net.minecraftforge.fml.event.server.FMLServerStartedEvent; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; + +@Mod("dynmap") +public class DynmapMod +{ + // The instance of your mod that Forge uses. + public static DynmapMod instance; + + // Says where the client and server 'proxy' code is loaded. + public static Proxy proxy = DistExecutor.runForDist(() -> ClientProxy::new, () -> Proxy::new); + + public static DynmapPlugin plugin; + public static File jarfile; + public static String ver; + public static boolean useforcedchunks; + + public class APICallback extends DynmapCommonAPIListener { + @Override + public void apiListenerAdded() { + if(plugin == null) { + plugin = proxy.startServer(server); + } + } + @Override + public void apiEnabled(DynmapCommonAPI api) { + } + } + + //TODO + //public class LoadingCallback implements net.minecraftforge.common.ForgeChunkManager.LoadingCallback { + // @Override + // public void ticketsLoaded(List tickets, World world) { + // if(tickets.size() > 0) { + // DynmapPlugin.setBusy(world, tickets.get(0)); + // for(int i = 1; i < tickets.size(); i++) { + // ForgeChunkManager.releaseTicket(tickets.get(i)); + // } + // } + // } + //} + + public DynmapMod() { + instance = this; + FMLJavaModLoadingContext.get().getModEventBus().addListener(this::setup); + FMLJavaModLoadingContext.get().getModEventBus().addListener(this::init); + + MinecraftForge.EVENT_BUS.register(this); + + Log.setLogger(new OurLog()); + org.dynmap.modsupport.ModSupportImpl.init(); + } + + public void setup(final FMLCommonSetupEvent event) + { + //TOOO + jarfile = ModList.get().getModFileById("dynmap").getFile().getFilePath().toFile(); + + ver = ModList.get().getModContainerById("dynmap").get().getModInfo().getVersion().toString(); + + //// Load configuration file - use suggested (config/WesterosBlocks.cfg) + //Configuration cfg = new Configuration(event.getSuggestedConfigurationFile()); + //try { + // cfg.load(); + // + // useforcedchunks = cfg.get("Settings", "UseForcedChunks", true).getBoolean(true); + //} + //finally + //{ + // cfg.save(); + //} + } + + public void init(FMLLoadCompleteEvent event) + { + /* Set up for chunk loading notice from chunk manager */ + //TODO + //if(useforcedchunks) { + // ForgeChunkManager.setForcedChunkLoadingCallback(DynmapMod.instance, new LoadingCallback()); + //} + //else { + // System.out.println("[Dynmap] World loading using forced chunks is disabled"); + //} + } + + private MinecraftServer server; + + @SubscribeEvent + public void onServerStarting(FMLServerStartingEvent event) { + server = event.getServer(); + if(plugin == null) + plugin = proxy.startServer(server); + plugin.onStarting(event.getCommandDispatcher()); + } + + @SubscribeEvent + public void onServerStarted(FMLServerStartedEvent event) { + DynmapCommonAPIListener.register(new APICallback()); + plugin.serverStarted(); + } + + @SubscribeEvent + public void serverStopping(FMLServerStoppingEvent event) + { + proxy.stopServer(plugin); + plugin = null; + } +} diff --git a/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/DynmapPlugin.java b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/DynmapPlugin.java new file mode 100644 index 00000000..e931d11b --- /dev/null +++ b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/DynmapPlugin.java @@ -0,0 +1,2093 @@ +package org.dynmap.forge_1_14_4; + +import java.io.File; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.PriorityQueue; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.regex.Pattern; + +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.material.Material; +import net.minecraft.command.CommandException; +import net.minecraft.command.CommandSource; +import net.minecraft.command.Commands; +import net.minecraft.entity.Entity; +import net.minecraft.item.Item; +import net.minecraft.network.NetworkManager; +import net.minecraft.particles.IParticleData; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.management.PlayerProfileCache; +import net.minecraft.util.ObjectIntIdentityMap; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.SoundCategory; +import net.minecraft.util.SoundEvent; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.util.text.ITextComponent; +import net.minecraft.world.IBlockReader; +import net.minecraft.world.IWorld; +import net.minecraft.world.World; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.ChunkSection; +import net.minecraft.world.chunk.IChunk; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.ServerChatEvent; +import net.minecraftforge.event.world.ChunkEvent; +import net.minecraftforge.event.world.WorldEvent; +import net.minecraftforge.fml.ModList; +import net.minecraftforge.fml.ModContainer; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.ForgeRegistry; +import net.minecraftforge.registries.RegistryManager; +import net.minecraftforge.fml.common.registry.GameRegistry; +import net.minecraftforge.fml.loading.moddiscovery.ModFileInfo; +import net.minecraftforge.fml.loading.moddiscovery.ModInfo; +import net.minecraftforge.forgespi.language.IModInfo; + +import org.apache.commons.codec.Charsets; +import org.apache.commons.codec.binary.Base64; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.maven.artifact.versioning.ArtifactVersion; +import org.dynmap.ConfigurationNode; +import org.dynmap.DynmapChunk; +import org.dynmap.DynmapCommonAPIListener; +import org.dynmap.DynmapCore; +import org.dynmap.DynmapLocation; +import org.dynmap.DynmapWorld; +import org.dynmap.Log; +import org.dynmap.MapManager; +import org.dynmap.PlayerList; +import org.dynmap.common.BiomeMap; +import org.dynmap.common.DynmapCommandSender; +import org.dynmap.common.DynmapPlayer; +import org.dynmap.common.DynmapServerInterface; +import org.dynmap.common.DynmapListenerManager.EventType; +import org.dynmap.debug.Debug; +import org.dynmap.forge_1_14_4.DmapCommand; +import org.dynmap.forge_1_14_4.DmarkerCommand; +import org.dynmap.forge_1_14_4.DynmapCommand; +import org.dynmap.forge_1_14_4.DynmapMod; +import org.dynmap.forge_1_14_4.permissions.FilePermissions; +import org.dynmap.forge_1_14_4.permissions.OpPermissions; +import org.dynmap.forge_1_14_4.permissions.PermissionProvider; +import org.dynmap.permissions.PermissionsHandler; +import org.dynmap.renderer.DynmapBlockState; +import org.dynmap.utils.DynIntHashMap; +import org.dynmap.utils.DynmapLogger; +import org.dynmap.utils.MapChunkCache; +import org.dynmap.utils.VisibilityLimit; + +import com.google.common.collect.Iterables; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; + +import net.minecraft.state.IProperty; +import net.minecraftforge.eventbus.api.SubscribeEvent; + +public class DynmapPlugin +{ + private DynmapCore core; + private PermissionProvider permissions; + private boolean core_enabled; + public SnapshotCache sscache; + public PlayerList playerList; + private MapManager mapManager; + private net.minecraft.server.MinecraftServer server; + public static DynmapPlugin plugin; + private ChatHandler chathandler; + private HashMap sortWeights = new HashMap(); + // Drop world load ticket after 30 seconds + private long worldIdleTimeoutNS = 30 * 1000000000L; + private HashMap worlds = new HashMap(); + private IWorld last_world; + private ForgeWorld last_fworld; + private Map players = new HashMap(); + //TODO private ForgeMetrics metrics; + private HashSet modsused = new HashSet(); + private ForgeServer fserver = new ForgeServer(); + private boolean tickregistered = false; + // TPS calculator + private double tps; + private long lasttick; + private long avgticklen; + // Per tick limit, in nsec + private long perTickLimit = (50000000); // 50 ms + private boolean isMCPC = false; + private boolean useSaveFolder = true; + private Field displayName = null; // MCPC+ display name + + private static final int SIGNPOST_ID = 63; + private static final int WALLSIGN_ID = 68; + + private static final String[] TRIGGER_DEFAULTS = { "blockupdate", "chunkpopulate", "chunkgenerate" }; + + private static final Pattern patternControlCode = Pattern.compile("(?i)\\u00A7[0-9A-FK-OR]"); + + public static class BlockUpdateRec { + IWorld w; + String wid; + int x, y, z; + } + ConcurrentLinkedQueue blockupdatequeue = new ConcurrentLinkedQueue(); + + public static DynmapBlockState[] stateByID; + + /** + * Initialize block states (org.dynmap.blockstate.DynmapBlockState) + */ + public void initializeBlockStates() { + stateByID = new DynmapBlockState[512*32]; // Simple map - scale as needed + Arrays.fill(stateByID, DynmapBlockState.AIR); // Default to air + + ObjectIntIdentityMap bsids = Block.BLOCK_STATE_IDS; + + DynmapBlockState basebs = null; + Block baseb = null; + int baseidx = 0; + + Iterator iter = bsids.iterator(); + while (iter.hasNext()) { + IBlockState bs = iter.next(); + int idx = bsids.get(bs); + if (idx >= stateByID.length) { + int plen = stateByID.length; + stateByID = Arrays.copyOf(stateByID, idx+1); + Arrays.fill(stateByID, plen, stateByID.length, DynmapBlockState.AIR); + } + Block b = bs.getBlock(); + // If this is new block vs last, it's the base block state + if (b != baseb) { + basebs = null; + baseidx = idx; + baseb = b; + } + + ResourceLocation ui = b.getRegistryName(); + if (ui == null) { + continue; + } + String bn = ui.getNamespace() + ":" + ui.getPath(); + // Only do defined names, and not "air" + if (!bn.equals(DynmapBlockState.AIR_BLOCK)) { + Material mat = bs.getMaterial(); + String statename = ""; + for(IProperty p : bs.getProperties()) { + if (statename.length() > 0) { + statename += ","; + } + statename += p.getName() + "=" + bs.get(p).toString(); + } + //Log.info("bn=" + bn + ", statenme=" + statename + ",idx=" + idx + ",baseidx=" + baseidx); + DynmapBlockState dbs = new DynmapBlockState(basebs, idx - baseidx, bn, statename, mat.toString(), idx); + stateByID[idx] = dbs; + if (basebs == null) { basebs = dbs; } + if (mat.isSolid()) { + dbs.setSolid(); + } + if (mat == Material.AIR) { + dbs.setAir(); + } + if (mat == Material.WOOD) { + dbs.setLog(); + } + if (mat == Material.LEAVES) { + dbs.setLeaves(); + } + } + } + for (int gidx = 0; gidx < DynmapBlockState.getGlobalIndexMax(); gidx++) { + DynmapBlockState bs = DynmapBlockState.getStateByGlobalIndex(gidx); + //Log.info(gidx + ":" + bs.toString() + ", gidx=" + bs.globalStateIndex + ", sidx=" + bs.stateIndex); + } + } + + public static final Item getItemByID(int id) { + return Item.getItemById(id); + } + + public static final String getBlockUnlocalizedName(Block b) { + return b.getNameTextComponent().getString(); + } + + private static Biome[] biomelist = null; + + public static final Biome[] getBiomeList() { + if (biomelist == null) { + biomelist = new Biome[256]; + Iterator iter = ForgeRegistries.BIOMES.iterator(); + while (iter.hasNext()) { + Biome b = iter.next(); + int bidx = RegistryNamespaced.BIOME.getId(b); + if (bidx >= biomelist.length) { + biomelist = Arrays.copyOf(biomelist, bidx + biomelist.length); + } + biomelist[bidx] = b; + } + } + return biomelist; + } + public static final NetworkManager getNetworkManager(NetHandlerPlayServer nh) { + return nh.netManager; + } + + private ForgePlayer getOrAddPlayer(EntityPlayer p) { + String name = p.getEntity().getName().getString(); + ForgePlayer fp = players.get(name); + if(fp != null) { + fp.player = p; + } + else { + fp = new ForgePlayer(p); + players.put(name, fp); + } + return fp; + } + + private static class TaskRecord implements Comparable + { + private long ticktorun; + private long id; + private FutureTask future; + @Override + public int compareTo(Object o) + { + TaskRecord tr = (TaskRecord)o; + + if (this.ticktorun < tr.ticktorun) + { + return -1; + } + else if (this.ticktorun > tr.ticktorun) + { + return 1; + } + else if (this.id < tr.id) + { + return -1; + } + else if (this.id > tr.id) + { + return 1; + } + else + { + return 0; + } + } + } + + private class ChatMessage { + String message; + EntityPlayer sender; + } + private ConcurrentLinkedQueue msgqueue = new ConcurrentLinkedQueue(); + + public class ChatHandler { + @SubscribeEvent + public void handleChat(ServerChatEvent event) { + String msg = event.getMessage(); + if(!msg.startsWith("/")) { + ChatMessage cm = new ChatMessage(); + cm.message = msg; + cm.sender = event.getPlayer(); + msgqueue.add(cm); + } + } + } + + /** TODO: depends on forge chunk manager + private static class WorldBusyRecord { + long last_ts; + Ticket ticket; + } + private static HashMap busy_worlds = new HashMap(); + + private void setBusy(World w) { + setBusy(w, null); + } + static void setBusy(World w, Ticket t) { + if(w == null) return; + if (!DynmapMod.useforcedchunks) return; + WorldBusyRecord wbr = busy_worlds.get(w.provider.getDimension()); + if(wbr == null) { // Not busy, make ticket and keep spawn loaded + Debug.debug("World " + w.getWorldInfo().getWorldName() + "/"+ w.provider.getDimensionType().getName() + " is busy"); + wbr = new WorldBusyRecord(); + if(t != null) + wbr.ticket = t; + else + wbr.ticket = ForgeChunkManager.requestTicket(DynmapMod.instance, w, ForgeChunkManager.Type.NORMAL); + if(wbr.ticket != null) { + BlockPos cc = w.getSpawnPoint(); + ChunkPos ccip = new ChunkPos(cc.getX() >> 4, cc.getZ() >> 4); + ForgeChunkManager.forceChunk(wbr.ticket, ccip); + busy_worlds.put(w.provider.getDimension(), wbr); // Add to busy list + } + } + wbr.last_ts = System.nanoTime(); + } + + private void doIdleOutOfWorlds() { + if (!DynmapMod.useforcedchunks) return; + long ts = System.nanoTime() - worldIdleTimeoutNS; + for(Iterator itr = busy_worlds.values().iterator(); itr.hasNext();) { + WorldBusyRecord wbr = itr.next(); + if(wbr.last_ts < ts) { + World w = wbr.ticket.world; + Debug.debug("World " + w.getWorldInfo().getWorldName() + "/" + wbr.ticket.world.provider.getDimensionType().getName() + " is idle"); + if (wbr.ticket != null) + ForgeChunkManager.releaseTicket(wbr.ticket); // Release hold on world + itr.remove(); + } + } + } + */ + + public static class OurLog implements DynmapLogger { + Logger log; + public static final String DM = "[Dynmap] "; + OurLog() { + log = LogManager.getLogger("Dynmap"); + } + @Override + public void info(String s) { + log.info(DM + s); + } + + @Override + public void severe(Throwable t) { + log.fatal(t); + } + + @Override + public void severe(String s) { + log.fatal(DM + s); + } + + @Override + public void severe(String s, Throwable t) { + log.fatal(DM + s, t); + } + + @Override + public void verboseinfo(String s) { + log.info(DM + s); + } + + @Override + public void warning(String s) { + log.warn(DM + s); + } + + @Override + public void warning(String s, Throwable t) { + log.warn(DM + s, t); + } + } + + public DynmapPlugin(MinecraftServer srv) + { + plugin = this; + this.server = srv; + + displayName = null; + try { + displayName = EntityPlayerMP.class.getField("displayName"); + } catch (SecurityException e) { + } catch (NoSuchFieldException e) { + } + } + + public boolean isOp(String player) { + String[] ops = server.getPlayerList().getOppedPlayers().getKeys(); + for (String op : ops) { + if (op.equalsIgnoreCase(player)) { + return true; + } + } + return (server.isSinglePlayer() && player.equalsIgnoreCase(server.getServerOwner())); + } + + private boolean hasPerm(EntityPlayer psender, String permission) { + PermissionsHandler ph = PermissionsHandler.getHandler(); + if((psender != null) && ph.hasPermission(psender.getEntity().getName().getString(), permission)) { + return true; + } + return permissions.has(psender, permission); + } + + private boolean hasPermNode(EntityPlayer psender, String permission) { + PermissionsHandler ph = PermissionsHandler.getHandler(); + if((psender != null) && ph.hasPermissionNode(psender.getEntity().getName().getString(), permission)) { + return true; + } + return permissions.hasPermissionNode(psender, permission); + } + + private Set hasOfflinePermissions(String player, Set perms) { + Set rslt = null; + PermissionsHandler ph = PermissionsHandler.getHandler(); + if(ph != null) { + rslt = ph.hasOfflinePermissions(player, perms); + } + Set rslt2 = hasOfflinePermissions(player, perms); + if((rslt != null) && (rslt2 != null)) { + Set newrslt = new HashSet(rslt); + newrslt.addAll(rslt2); + rslt = newrslt; + } + else if(rslt2 != null) { + rslt = rslt2; + } + return rslt; + } + private boolean hasOfflinePermission(String player, String perm) { + PermissionsHandler ph = PermissionsHandler.getHandler(); + if(ph != null) { + if(ph.hasOfflinePermission(player, perm)) { + return true; + } + } + return permissions.hasOfflinePermission(player, perm); + } + + /** + * Server access abstraction class + */ + public class ForgeServer extends DynmapServerInterface + { + /* Server thread scheduler */ + private Object schedlock = new Object(); + private long cur_tick; + private long next_id; + private long cur_tick_starttime; + private PriorityQueue runqueue = new PriorityQueue(); + + public ForgeServer() { + } + + private GameProfile getProfileByName(String player) { + PlayerProfileCache cache = server.getPlayerProfileCache(); + return cache.getGameProfileForUsername(player); + } + + @Override + public int getBlockIDAt(String wname, int x, int y, int z) { + return -1; + } + + @Override + public int isSignAt(String wname, int x, int y, int z) { + return -1; + } + + @Override + public void scheduleServerTask(Runnable run, long delay) + { + TaskRecord tr = new TaskRecord(); + tr.future = new FutureTask(run, null); + + /* Add task record to queue */ + synchronized (schedlock) + { + tr.id = next_id++; + tr.ticktorun = cur_tick + delay; + runqueue.add(tr); + } + } + @Override + public DynmapPlayer[] getOnlinePlayers() + { + if(server.getPlayerList() == null) + return new DynmapPlayer[0]; + List playlist = server.getPlayerList().getPlayers(); + int pcnt = playlist.size(); + DynmapPlayer[] dplay = new DynmapPlayer[pcnt]; + + for (int i = 0; i < pcnt; i++) + { + EntityPlayer p = (EntityPlayer)playlist.get(i); + dplay[i] = getOrAddPlayer(p); + } + + return dplay; + } + @Override + public void reload() + { + plugin.onDisable(); + plugin.onEnable(); + plugin.onStart(); + } + @Override + public DynmapPlayer getPlayer(String name) + { + List players = server.getPlayerList().getPlayers(); + + for (Object o : players) + { + EntityPlayer p = (EntityPlayer)o; + + if (p.getEntity().getName().getString().equalsIgnoreCase(name)) + { + return getOrAddPlayer(p); + } + } + + return null; + } + @Override + public Set getIPBans() + { + UserListIPBans bl = server.getPlayerList().getBannedIPs(); + Set ips = new HashSet(); + + for (String s : bl.getKeys()) { + ips.add(s); + } + + return ips; + } + @Override + public Future callSyncMethod(Callable task) { + return callSyncMethod(task, 0); + } + public Future callSyncMethod(Callable task, long delay) + { + TaskRecord tr = new TaskRecord(); + FutureTask ft = new FutureTask(task); + tr.future = ft; + + /* Add task record to queue */ + synchronized (schedlock) + { + tr.id = next_id++; + tr.ticktorun = cur_tick + delay; + runqueue.add(tr); + } + + return ft; + } + @Override + public String getServerName() + { + String sn; + if (server.isSinglePlayer()) + sn = "Integrated"; + else + sn = server.getServerHostname(); + if(sn == null) sn = "Unknown Server"; + return sn; + } + @Override + public boolean isPlayerBanned(String pid) + { + UserListBans bl = server.getPlayerList().getBannedPlayers(); + return bl.isBanned(getProfileByName(pid)); + } + + @Override + public String stripChatColor(String s) + { + return patternControlCode.matcher(s).replaceAll(""); + } + private Set registered = new HashSet(); + @Override + public boolean requestEventNotification(EventType type) + { + if (registered.contains(type)) + { + return true; + } + + switch (type) + { + case WORLD_LOAD: + case WORLD_UNLOAD: + /* Already called for normal world activation/deactivation */ + break; + + case WORLD_SPAWN_CHANGE: + /*TODO + pm.registerEvents(new Listener() { + @EventHandler(priority=EventPriority.MONITOR) + public void onSpawnChange(SpawnChangeEvent evt) { + DynmapWorld w = new BukkitWorld(evt.getWorld()); + core.listenerManager.processWorldEvent(EventType.WORLD_SPAWN_CHANGE, w); + } + }, DynmapPlugin.this); + */ + break; + + case PLAYER_JOIN: + case PLAYER_QUIT: + /* Already handled */ + break; + + case PLAYER_BED_LEAVE: + /*TODO + pm.registerEvents(new Listener() { + @EventHandler(priority=EventPriority.MONITOR) + public void onPlayerBedLeave(PlayerBedLeaveEvent evt) { + DynmapPlayer p = new BukkitPlayer(evt.getPlayer()); + core.listenerManager.processPlayerEvent(EventType.PLAYER_BED_LEAVE, p); + } + }, DynmapPlugin.this); + */ + break; + + case PLAYER_CHAT: + if (chathandler == null) { + chathandler = new ChatHandler(); + MinecraftForge.EVENT_BUS.register(chathandler); + } + break; + + case BLOCK_BREAK: + /*TODO + pm.registerEvents(new Listener() { + @EventHandler(priority=EventPriority.MONITOR) + public void onBlockBreak(BlockBreakEvent evt) { + if(evt.isCancelled()) return; + Block b = evt.getBlock(); + if(b == null) return; + Location l = b.getLocation(); + core.listenerManager.processBlockEvent(EventType.BLOCK_BREAK, b.getType().getId(), + BukkitWorld.normalizeWorldName(l.getWorld().getName()), l.getBlockX(), l.getBlockY(), l.getBlockZ()); + } + }, DynmapPlugin.this); + */ + break; + + case SIGN_CHANGE: + /*TODO + pm.registerEvents(new Listener() { + @EventHandler(priority=EventPriority.MONITOR) + public void onSignChange(SignChangeEvent evt) { + if(evt.isCancelled()) return; + Block b = evt.getBlock(); + Location l = b.getLocation(); + String[] lines = evt.getLines(); + DynmapPlayer dp = null; + Player p = evt.getPlayer(); + if(p != null) dp = new BukkitPlayer(p); + core.listenerManager.processSignChangeEvent(EventType.SIGN_CHANGE, b.getType().getId(), + BukkitWorld.normalizeWorldName(l.getWorld().getName()), l.getBlockX(), l.getBlockY(), l.getBlockZ(), lines, dp); + } + }, DynmapPlugin.this); + */ + break; + + default: + Log.severe("Unhandled event type: " + type); + return false; + } + + registered.add(type); + return true; + } + @Override + public boolean sendWebChatEvent(String source, String name, String msg) + { + return DynmapCommonAPIListener.fireWebChatEvent(source, name, msg); + } + @Override + public void broadcastMessage(String msg) + { + ITextComponent component = new TextComponentString(msg); + server.getPlayerList().sendMessage(component); + Log.info(stripChatColor(msg)); + } + @Override + public String[] getBiomeIDs() + { + BiomeMap[] b = BiomeMap.values(); + String[] bname = new String[b.length]; + + for (int i = 0; i < bname.length; i++) + { + bname[i] = b[i].toString(); + } + + return bname; + } + @Override + public double getCacheHitRate() + { + if(sscache != null) + return sscache.getHitRate(); + return 0.0; + } + @Override + public void resetCacheStats() + { + if(sscache != null) + sscache.resetStats(); + } + @Override + public DynmapWorld getWorldByName(String wname) + { + return DynmapPlugin.this.getWorldByName(wname); + } + @Override + public DynmapPlayer getOfflinePlayer(String name) + { + /* + OfflinePlayer op = getServer().getOfflinePlayer(name); + if(op != null) { + return new BukkitPlayer(op); + } + */ + return null; + } + @Override + public Set checkPlayerPermissions(String player, Set perms) + { + net.minecraft.server.management.PlayerList scm = server.getPlayerList(); + if (scm == null) return Collections.emptySet(); + UserListBans bl = scm.getBannedPlayers(); + if (bl == null) return Collections.emptySet(); + if(bl.isBanned(getProfileByName(player))) { + return Collections.emptySet(); + } + Set rslt = hasOfflinePermissions(player, perms); + if (rslt == null) { + rslt = new HashSet(); + if(plugin.isOp(player)) { + rslt.addAll(perms); + } + } + return rslt; + } + @Override + public boolean checkPlayerPermission(String player, String perm) + { + net.minecraft.server.management.PlayerList scm = server.getPlayerList(); + if (scm == null) return false; + UserListBans bl = scm.getBannedPlayers(); + if (bl == null) return false; + if(bl.isBanned(getProfileByName(player))) { + return false; + } + return hasOfflinePermission(player, perm); + } + /** + * Render processor helper - used by code running on render threads to request chunk snapshot cache from server/sync thread + */ + @Override + public MapChunkCache createMapChunkCache(DynmapWorld w, List chunks, + boolean blockdata, boolean highesty, boolean biome, boolean rawbiome) + { + ForgeMapChunkCache c = (ForgeMapChunkCache) w.getChunkCache(chunks); + if(c == null) { + return null; + } + if (w.visibility_limits != null) + { + for (VisibilityLimit limit: w.visibility_limits) + { + c.setVisibleRange(limit); + } + + c.setHiddenFillStyle(w.hiddenchunkstyle); + } + + if (w.hidden_limits != null) + { + for (VisibilityLimit limit: w.hidden_limits) + { + c.setHiddenRange(limit); + } + + c.setHiddenFillStyle(w.hiddenchunkstyle); + } + + if (c.setChunkDataTypes(blockdata, biome, highesty, rawbiome) == false) + { + Log.severe("CraftBukkit build does not support biome APIs"); + } + + if (chunks.size() == 0) /* No chunks to get? */ + { + c.loadChunks(0); + return c; + } + + //Now handle any chunks in server thread that are already loaded (on server thread) + final ForgeMapChunkCache cc = c; + Future f = this.callSyncMethod(new Callable() { + public Boolean call() throws Exception { + // Update busy state on world + ForgeWorld fw = (ForgeWorld)cc.getWorld(); + //TODO + //setBusy(fw.getWorld()); + cc.getLoadedChunks(); + return true; + } + }, 0); + try { + f.get(); + } + catch (CancellationException cx) { + return null; + } + catch (ExecutionException xx) { + Log.severe("Exception while loading chunks", xx.getCause()); + return null; + } + catch (Exception ix) { + Log.severe(ix); + return null; + } + if(w.isLoaded() == false) { + return null; + } + // Now, do rest of chunk reading from calling thread + c.readChunks(chunks.size()); + + return c; + } + @Override + public int getMaxPlayers() + { + return server.getMaxPlayers(); + } + @Override + public int getCurrentPlayers() + { + return server.getPlayerList().getCurrentPlayerCount(); + } + + @SubscribeEvent + public void tickEvent(TickEvent.ServerTickEvent event) { + if (event.phase == TickEvent.Phase.START) { + return; + } + cur_tick_starttime = System.nanoTime(); + long elapsed = cur_tick_starttime - lasttick; + lasttick = cur_tick_starttime; + avgticklen = ((avgticklen * 99) / 100) + (elapsed / 100); + tps = (double)1E9 / (double)avgticklen; + // Tick core + if (core != null) { + core.serverTick(tps); + } + + boolean done = false; + TaskRecord tr = null; + + while(!blockupdatequeue.isEmpty()) { + BlockUpdateRec r = blockupdatequeue.remove(); + IBlockState bs = r.w.getBlockState(new BlockPos(r.x, r.y, r.z)); + int idx = Block.BLOCK_STATE_IDS.get(bs); + if(!org.dynmap.hdmap.HDBlockModels.isChangeIgnoredBlock(stateByID[idx])) { + if(onblockchange_with_id) + mapManager.touch(r.wid, r.x, r.y, r.z, "blockchange[" + idx + "]"); + else + mapManager.touch(r.wid, r.x, r.y, r.z, "blockchange"); + } + } + + long now; + + synchronized(schedlock) { + cur_tick++; + now = System.nanoTime(); + tr = runqueue.peek(); + /* Nothing due to run */ + if((tr == null) || (tr.ticktorun > cur_tick) || ((now - cur_tick_starttime) > perTickLimit)) { + done = true; + } + else { + tr = runqueue.poll(); + } + } + while (!done) { + tr.future.run(); + + synchronized(schedlock) { + tr = runqueue.peek(); + now = System.nanoTime(); + /* Nothing due to run */ + if((tr == null) || (tr.ticktorun > cur_tick) || ((now - cur_tick_starttime) > perTickLimit)) { + done = true; + } + else { + tr = runqueue.poll(); + } + } + } + while(!msgqueue.isEmpty()) { + ChatMessage cm = msgqueue.poll(); + DynmapPlayer dp = null; + if(cm.sender != null) + dp = getOrAddPlayer(cm.sender); + else + dp = new ForgePlayer(null); + + core.listenerManager.processChatEvent(EventType.PLAYER_CHAT, dp, cm.message); + } + /* Check for idle worlds */ + if((cur_tick % 20) == 0) { + //TODO + //doIdleOutOfWorlds(); + } + } + + @Override + public boolean isModLoaded(String name) { + boolean loaded = ModList.get().isLoaded(name); + if (loaded) { + modsused.add(name); + } + return loaded; + } + @Override + public String getModVersion(String name) { + Optional mod = ModList.get().getModContainerById(name); // Try case sensitive lookup + if (mod.isPresent()) { + ArtifactVersion vi = mod.get().getModInfo().getVersion(); + return vi.getMajorVersion() + "." + vi.getMinorVersion() + "." + vi.getIncrementalVersion(); + } + return null; + } + @Override + public double getServerTPS() { + return tps; + } + + @Override + public String getServerIP() { + if (server.isSinglePlayer()) + return "0.0.0.0"; + else + return server.getServerHostname(); + } + @Override + public File getModContainerFile(String name) { + ModFileInfo mfi = ModList.get().getModFileById(name); // Try case sensitive lookup + if (mfi != null) { + File f = mfi.getFile().getFilePath().toFile(); + return f; + } + return null; + } + @Override + public List getModList() { + List mil = ModList.get().getMods(); + List lst = new ArrayList(); + for (ModInfo mi : mil) { + lst.add(mi.getModId()); + } + return lst; + } + + @Override + public Map getBlockIDMap() { + Map map = new HashMap(); + return map; + } + + @Override + public InputStream openResource(String modid, String rname) { + if (modid != null) { + Optional mc = ModList.get().getModContainerById(modid); + Object mod = (mc.isPresent()) ? mc.get().getMod() : null; + if (mod != null) { + InputStream is = mod.getClass().getClassLoader().getResourceAsStream(rname); + if (is != null) { + return is; + } + } + } + List mcl = ModList.get().getMods(); + for (ModInfo mci : mcl) { + Optional mc = ModList.get().getModContainerById(mci.getModId()); + Object mod = (mc.isPresent()) ? mc.get().getMod() : null; + if (mod == null) continue; + InputStream is = mod.getClass().getClassLoader().getResourceAsStream(rname); + if (is != null) { + return is; + } + } + return null; + } + /** + * Get block unique ID map (module:blockid) + */ + @Override + public Map getBlockUniqueIDMap() { + HashMap map = new HashMap(); + return map; + } + /** + * Get item unique ID map (module:itemid) + */ + @Override + public Map getItemUniqueIDMap() { + HashMap map = new HashMap(); + return map; + } + + } + private static final Gson gson = new GsonBuilder().create(); + + public class TexturesPayload { + public long timestamp; + public String profileId; + public String profileName; + public boolean isPublic; + public Map textures; + + } + public class ProfileTexture { + public String url; + } + + /** + * Player access abstraction class + */ + public class ForgePlayer extends ForgeCommandSender implements DynmapPlayer + { + private EntityPlayer player; + private final String skinurl; + private final UUID uuid; + + + public ForgePlayer(EntityPlayer p) + { + player = p; + String url = null; + if (player != null) { + uuid = player.getUniqueID(); + GameProfile prof = player.getGameProfile(); + if (prof != null) { + Property textureProperty = Iterables.getFirst(prof.getProperties().get("textures"), null); + + if (textureProperty != null) { + TexturesPayload result = null; + try { + String json = new String(Base64.decodeBase64(textureProperty.getValue()), Charsets.UTF_8); + result = gson.fromJson(json, TexturesPayload.class); + } catch (JsonParseException e) { + } + if ((result != null) && (result.textures != null) && (result.textures.containsKey("SKIN"))) { + url = result.textures.get("SKIN").url; + } + } + } + } + else { + uuid = null; + } + skinurl = url; + } + @Override + public boolean isConnected() + { + return true; + } + @Override + public String getName() + { + if(player != null) + return player.getEntity().getName().getString(); + else + return "[Server]"; + } + @Override + public String getDisplayName() + { + if(player != null) { + if (displayName != null) { + try { + return (String) displayName.get(player); + } catch (IllegalArgumentException e) { + } catch (IllegalAccessException e) { + } + } + return player.getDisplayName().getUnformattedComponentText(); + } + else + return "[Server]"; + } + @Override + public boolean isOnline() + { + return true; + } + @Override + public DynmapLocation getLocation() + { + if (player == null) + { + return null; + } + + return toLoc(player.world, player.posX, player.posY, player.posZ); + } + @Override + public String getWorld() + { + if (player == null) + { + return null; + } + + if (player.world != null) + { + return DynmapPlugin.this.getWorld(player.world).getName(); + } + + return null; + } + @Override + public InetSocketAddress getAddress() + { + if((player != null) && (player instanceof EntityPlayerMP)) { + NetHandlerPlayServer nsh = ((EntityPlayerMP)player).connection; + if((nsh != null) && (getNetworkManager(nsh) != null)) { + SocketAddress sa = getNetworkManager(nsh).getRemoteAddress(); + if(sa instanceof InetSocketAddress) { + return (InetSocketAddress)sa; + } + } + } + return null; + } + @Override + public boolean isSneaking() + { + if (player != null) + { + return player.isSneaking(); + } + + return false; + } + @Override + public double getHealth() + { + if (player != null) + { + double h = player.getHealth(); + if(h > 20) h = 20; + return h; // Scale to 20 range + } + else + { + return 0; + } + } + @Override + public int getArmorPoints() + { + if (player != null) + { + return player.getTotalArmorValue(); + } + else + { + return 0; + } + } + @Override + public DynmapLocation getBedSpawnLocation() + { + return null; + } + @Override + public long getLastLoginTime() + { + return 0; + } + @Override + public long getFirstLoginTime() + { + return 0; + } + @Override + public boolean hasPrivilege(String privid) + { + if(player != null) + return hasPerm(player, privid); + return false; + } + @Override + public boolean isOp() + { + return DynmapPlugin.this.isOp(player.getEntity().getName().getString()); + } + @Override + public void sendMessage(String msg) + { + ITextComponent ichatcomponent = new TextComponentString(msg); + player.sendMessage(ichatcomponent); + } + @Override + public boolean isInvisible() { + if(player != null) { + return player.isInvisible(); + } + return false; + } + @Override + public int getSortWeight() { + Integer wt = sortWeights.get(getName()); + if (wt != null) + return wt; + return 0; + } + @Override + public void setSortWeight(int wt) { + if (wt == 0) { + sortWeights.remove(getName()); + } + else { + sortWeights.put(getName(), wt); + } + } + @Override + public boolean hasPermissionNode(String node) { + if(player != null) + return hasPermNode(player, node); + return false; + } + @Override + public String getSkinURL() { + return skinurl; + } + @Override + public UUID getUUID() { + return uuid; + } + } + /* Handler for generic console command sender */ + public class ForgeCommandSender implements DynmapCommandSender + { + private CommandSource sender; + + protected ForgeCommandSender() { + sender = null; + } + + public ForgeCommandSender(CommandSource send) + { + sender = send; + } + + @Override + public boolean hasPrivilege(String privid) + { + return true; + } + + @Override + public void sendMessage(String msg) + { + if(sender != null) { + ITextComponent ichatcomponent = new TextComponentString(msg); + sender.sendFeedback(ichatcomponent, false); + } + } + + @Override + public boolean isConnected() + { + return false; + } + @Override + public boolean isOp() + { + return true; + } + @Override + public boolean hasPermissionNode(String node) { + return true; + } + } + + public void loadExtraBiomes(String mcver) { + int cnt = 0; + BiomeMap.loadWellKnownByVersion(mcver); + + Biome[] list = getBiomeList(); + + for(int i = 0; i < list.length; i++) { + Biome bb = list[i]; + if(bb != null) { + String id = bb.getRegistryName().getPath(); + float tmp = bb.getDefaultTemperature(), hum = bb.getDownfall(); + int watermult = bb.getWaterColor(); + Log.verboseinfo("biome[" + i + "]: hum=" + hum + ", tmp=" + tmp + ", mult=" + Integer.toHexString(watermult)); + + BiomeMap bmap = BiomeMap.byBiomeID(i); + if (bmap.isDefault()) { + bmap = new BiomeMap(i, id, tmp, hum); + Log.verboseinfo("Add custom biome [" + bmap.toString() + "] (" + i + ")"); + cnt++; + } + else { + bmap.setTemperature(tmp); + bmap.setRainfall(hum); + } + if (watermult != -1) { + bmap.setWaterColorMultiplier(watermult); + Log.verboseinfo("Set watercolormult for " + bmap.toString() + " (" + i + ") to " + Integer.toHexString(watermult)); + } + } + } + if(cnt > 0) + Log.info("Added " + cnt + " custom biome mappings"); + } + + private String[] getBiomeNames() { + Biome[] list = getBiomeList(); + String[] lst = new String[list.length]; + for(int i = 0; i < list.length; i++) { + Biome bb = list[i]; + if (bb != null) { + lst[i] = bb.getRegistryName().getPath(); + } + } + return lst; + } + + public void onEnable() + { + /* Get MC version */ + String mcver = server.getMinecraftVersion(); + + /* Load extra biomes */ + loadExtraBiomes(mcver); + /* Set up player login/quit event handler */ + registerPlayerLoginListener(); + /* Initialize permissions handler */ + permissions = FilePermissions.create(); + if(permissions == null) { + permissions = new OpPermissions(new String[] { "webchat", "marker.icons", "marker.list", "webregister", "stats", "hide.self", "show.self" }); + } + /* Get and initialize data folder */ + File dataDirectory = new File("dynmap"); + + if (dataDirectory.exists() == false) + { + dataDirectory.mkdirs(); + } + + /* Instantiate core */ + if (core == null) + { + core = new DynmapCore(); + } + + /* Inject dependencies */ + core.setPluginJarFile(DynmapMod.jarfile); + core.setPluginVersion(DynmapMod.ver); + core.setMinecraftVersion(mcver); + core.setDataFolder(dataDirectory); + core.setServer(fserver); + ForgeMapChunkCache.init(); + core.setTriggerDefault(TRIGGER_DEFAULTS); + core.setBiomeNames(getBiomeNames()); + + if(!core.initConfiguration(null)) + { + return; + } + DynmapCommonAPIListener.apiInitialized(core); + } + + private static int test(CommandSource source) throws CommandSyntaxException + { + System.out.println(source.toString()); + return 1; + } + + private DynmapCommand dynmapCmd; + private DmapCommand dmapCmd; + private DmarkerCommand dmarkerCmd; + private DynmapExpCommand dynmapexpCmd; + + public void onStarting(CommandDispatcher cd) { + /* Register command hander */ + dynmapCmd = new DynmapCommand(this); + dmapCmd = new DmapCommand(this); + dmarkerCmd = new DmarkerCommand(this); + dynmapexpCmd = new DynmapExpCommand(this); + dynmapCmd.register(cd); + dmapCmd.register(cd); + dmarkerCmd.register(cd); + dynmapexpCmd.register(cd); + + Log.info("Register commands"); + } + + public void onStart() { + initializeBlockStates(); + /* Enable core */ + if (!core.enableCore(null)) + { + return; + } + core_enabled = true; + VersionCheck.runCheck(core); + // Get per tick time limit + perTickLimit = core.getMaxTickUseMS() * 1000000; + // Prep TPS + lasttick = System.nanoTime(); + tps = 20.0; + + /* Register tick handler */ + if(!tickregistered) { + MinecraftForge.EVENT_BUS.register(fserver); + tickregistered = true; + } + + playerList = core.playerList; + sscache = new SnapshotCache(core.getSnapShotCacheSize(), core.useSoftRefInSnapShotCache()); + /* Get map manager from core */ + mapManager = core.getMapManager(); + + /* Load saved world definitions */ + loadWorlds(); + + /* Initialized the currently loaded worlds */ + if(server.getWorlds() != null) { + for (WorldServer world : server.getWorlds()) { + ForgeWorld w = this.getWorld(world); + /*NOTYET - need rest of forge + if(DimensionManager.getWorld(world.provider.getDimensionId()) == null) { // If not loaded + w.setWorldUnloaded(); + } + */ + } + } + for(ForgeWorld w : worlds.values()) { + if (core.processWorldLoad(w)) { /* Have core process load first - fire event listeners if good load after */ + if(w.isLoaded()) { + core.listenerManager.processWorldEvent(EventType.WORLD_LOAD, w); + } + } + } + core.updateConfigHashcode(); + + /* Register our update trigger events */ + registerEvents(); + Log.info("Register events"); + + /* Submit metrics to mcstats.org */ + initMetrics(); + + //DynmapCommonAPIListener.apiInitialized(core); + + Log.info("Enabled"); + } + + public void onDisable() + { + DynmapCommonAPIListener.apiTerminated(); + + //if (metrics != null) { + // metrics.stop(); + // metrics = null; + //} + /* Save worlds */ + saveWorlds(); + + /* Purge tick queue */ + fserver.runqueue.clear(); + + /* Disable core */ + core.disableCore(); + core_enabled = false; + + if (sscache != null) + { + sscache.cleanup(); + sscache = null; + } + + Log.info("Disabled"); + } + + void onCommand(CommandSource sender, String cmd, String[] args) + { + DynmapCommandSender dsender; + EntityPlayer psender; + try { + psender = sender.asPlayer(); + } catch (com.mojang.brigadier.exceptions.CommandSyntaxException x) { + psender = null; + } + + if (psender != null) + { + dsender = new ForgePlayer(psender); + } + else + { + dsender = new ForgeCommandSender(sender); + } + + core.processCommand(dsender, cmd, cmd, args); + } + + private DynmapLocation toLoc(World worldObj, double x, double y, double z) + { + return new DynmapLocation(DynmapPlugin.this.getWorld(worldObj).getName(), x, y, z); + } + + public class PlayerTracker { + @SubscribeEvent + public void onPlayerLogin(PlayerLoggedInEvent event) { + if(!core_enabled) return; + final DynmapPlayer dp = getOrAddPlayer(event.getPlayer()); + /* This event can be called from off server thread, so push processing there */ + core.getServer().scheduleServerTask(new Runnable() { + public void run() { + core.listenerManager.processPlayerEvent(EventType.PLAYER_JOIN, dp); + } + }, 2); + } + @SubscribeEvent + public void onPlayerLogout(PlayerLoggedOutEvent event) { + if(!core_enabled) return; + final DynmapPlayer dp = getOrAddPlayer(event.getPlayer()); + final String name = event.getPlayer().getEntity().getName().getString(); + /* This event can be called from off server thread, so push processing there */ + core.getServer().scheduleServerTask(new Runnable() { + public void run() { + core.listenerManager.processPlayerEvent(EventType.PLAYER_QUIT, dp); + players.remove(name); + } + }, 0); + } + @SubscribeEvent + public void onPlayerChangedDimension(PlayerChangedDimensionEvent event) { + if(!core_enabled) return; + getOrAddPlayer(event.getPlayer()); // Freshen player object reference + } + @SubscribeEvent + public void onPlayerRespawn(PlayerRespawnEvent event) { + if(!core_enabled) return; + getOrAddPlayer(event.getPlayer()); // Freshen player object reference + } + } + private PlayerTracker playerTracker = null; + + private void registerPlayerLoginListener() + { + if (playerTracker == null) { + playerTracker = new PlayerTracker(); + MinecraftForge.EVENT_BUS.register(playerTracker); + } + } + + public class WorldTracker { + @SubscribeEvent + public void handleWorldLoad(WorldEvent.Load event) { + if(!core_enabled) return; + IWorld w = event.getWorld(); + if(!(w instanceof WorldServer)) return; + final ForgeWorld fw = getWorld(w); + // This event can be called from off server thread, so push processing there + core.getServer().scheduleServerTask(new Runnable() { + public void run() { + if(core.processWorldLoad(fw)) // Have core process load first - fire event listeners if good load after + core.listenerManager.processWorldEvent(EventType.WORLD_LOAD, fw); + } + }, 0); + } + @SubscribeEvent + public void handleWorldUnload(WorldEvent.Unload event) { + if(!core_enabled) return; + IWorld w = event.getWorld(); + if(!(w instanceof WorldServer)) return; + final ForgeWorld fw = getWorld(w); + if(fw != null) { + // This event can be called from off server thread, so push processing there + core.getServer().scheduleServerTask(new Runnable() { + public void run() { + core.listenerManager.processWorldEvent(EventType.WORLD_UNLOAD, fw); + core.processWorldUnload(fw); + } + }, 0); + // Set world unloaded (needs to be immediate, since it may be invalid after event) + fw.setWorldUnloaded(); + // Clean up tracker + WorldUpdateTracker wut = updateTrackers.remove(fw.getName()); + if(wut != null) wut.world = null; + } + } + + @SubscribeEvent + public void handleChunkLoad(ChunkEvent.Load event) { + if(!core_enabled) return; + if(!onchunkgenerate) return; + IWorld w = event.getWorld(); + if(!(w instanceof WorldServer)) return; + IChunk c = event.getChunk(); + if((c != null) /*TODO && (!c.isTerrainPopulated())*/) { // If new chunk? + ForgeWorld fw = getWorld(w, false); + if(fw == null) { + return; + } + int ymax = 0; + ChunkSection[] sections = c.getSections(); + for(int i = 0; i < sections.length; i++) { + if((sections[i] != null) && (sections[i].isEmpty() == false)) { + ymax = 16*(i+1); + } + } + ChunkPos cp = c.getPos(); + int x = cp.x << 4; + int z = cp.z << 4; + if(ymax > 0) { + mapManager.touchVolume(fw.getName(), x, 0, z, x+15, ymax, z+16, "chunkgenerate"); + } + } + } + + /*TODO + @SubscribeEvent + public void handleChunkPopulate(PopulateChunkEvent.Post event) { + if(!core_enabled) return; + if(!onchunkpopulate) return; + World w = event.getWorld(); + if(!(w instanceof WorldServer)) return; + Chunk c = w.getChunkFromChunkCoords(event.getChunkX(), event.getChunkZ()); + int ymin = 0, ymax = 0; + if(c != null) { + ForgeWorld fw = getWorld(event.getWorld(), false); + if (fw == null) return; + + ExtendedBlockStorage[] sections = c.getBlockStorageArray(); + for(int i = 0; i < sections.length; i++) { + if((sections[i] != null) && (sections[i].isEmpty() == false)) { + ymax = 16*(i+1); + } + } + int x = c.x << 4; + int z = c.z << 4; + if(ymax > 0) + mapManager.touchVolume(fw.getName(), x, ymin, z, x+15, ymax, z+16, "chunkpopulate"); + } + } + */ + } + + private boolean onblockchange = false; + private boolean onlightingchange = false; + private boolean onchunkpopulate = false; + private boolean onchunkgenerate = false; + private boolean onblockchange_with_id = false; + + + public class WorldUpdateTracker implements IWorldEventListener { + String worldid; + IWorld world; + @Override + public void notifyLightSet(BlockPos pos) { + if(sscache != null) + sscache.invalidateSnapshot(worldid, pos.getX(), pos.getY(), pos.getZ()); + if(onlightingchange) { + mapManager.touch(worldid, pos.getX(), pos.getY(), pos.getZ(), "lightingchange"); + } + } + @Override + public void markBlockRangeForRenderUpdate(int x1, int y1, int z1, int x2, int y2, int z2) { + } + @Override + public void onEntityAdded(Entity entityIn) { + } + @Override + public void onEntityRemoved(Entity entityIn) { + } + @Override + public void sendBlockBreakProgress(int breakerId, BlockPos pos, + int progress) { + } + @Override + public void broadcastSound(int p_180440_1_, BlockPos p_180440_2_, + int p_180440_3_) { + } + @Override + public void playSoundToAllNearExcept(EntityPlayer player, + SoundEvent soundIn, SoundCategory category, double x, double y, + double z, float volume, float pitch) { + } + @Override + public void playRecord(SoundEvent soundIn, BlockPos pos) { + } + @Override + public void playEvent(EntityPlayer arg0, int arg1, BlockPos arg2, int arg3) { + } + @Override + public void notifyBlockUpdate(IBlockReader worldIn, BlockPos pos, IBlockState oldState, IBlockState newState, + int flags) { + if(sscache != null) + sscache.invalidateSnapshot(worldid, pos.getX(), pos.getY(), pos.getZ()); + if(onblockchange) { + BlockUpdateRec r = new BlockUpdateRec(); + r.w = world; + r.wid = worldid; + r.x = pos.getX(); r.y = pos.getY(); r.z = pos.getZ(); + blockupdatequeue.add(r); + } + } + @Override + public void addParticle(IParticleData particleData, boolean alwaysRender, double x, double y, double z, + double xSpeed, double ySpeed, double zSpeed) { + // TODO Auto-generated method stub + + } + @Override + public void addParticle(IParticleData particleData, boolean ignoreRange, boolean minimizeLevel, double x, + double y, double z, double xSpeed, double ySpeed, double zSpeed) { + // TODO Auto-generated method stub + + } + } + + private WorldTracker worldTracker = null; + private HashMap updateTrackers = new HashMap(); + + private void registerEvents() + { + if(worldTracker == null) { + worldTracker = new WorldTracker(); + MinecraftForge.EVENT_BUS.register(worldTracker); + } + // To trigger rendering. + onblockchange = core.isTrigger("blockupdate"); + onlightingchange = core.isTrigger("lightingupdate"); + onchunkpopulate = core.isTrigger("chunkpopulate"); + onchunkgenerate = core.isTrigger("chunkgenerate"); + onblockchange_with_id = core.isTrigger("blockupdate-with-id"); + if(onblockchange_with_id) + onblockchange = true; + } + + private ForgeWorld getWorldByName(String name) { + return worlds.get(name); + } + + private ForgeWorld getWorld(IWorld w) { + return getWorld(w, true); + } + + private ForgeWorld getWorld(IWorld w, boolean add_if_not_found) { + if(last_world == w) { + return last_fworld; + } + String wname = ForgeWorld.getWorldName(w); + + for(ForgeWorld fw : worlds.values()) { + if(fw.getRawName().equals(wname)) { + last_world = w; + last_fworld = fw; + if(fw.isLoaded() == false) { + fw.setWorldLoaded(w); + // Add tracker + WorldUpdateTracker wit = new WorldUpdateTracker(); + wit.worldid = fw.getName(); + wit.world = w; + updateTrackers.put(fw.getName(), wit); + w.getWorld().addEventListener(wit); + } + return fw; + } + } + ForgeWorld fw = null; + if(add_if_not_found) { + /* Add to list if not found */ + fw = new ForgeWorld(w); + worlds.put(fw.getName(), fw); + // Add tracker + WorldUpdateTracker wit = new WorldUpdateTracker(); + wit.worldid = fw.getName(); + wit.world = w; + updateTrackers.put(fw.getName(), wit); + w.getWorld().addEventListener(wit); + } + last_world = w; + last_fworld = fw; + return fw; + } + + /* + private void removeWorld(ForgeWorld fw) { + WorldUpdateTracker wit = updateTrackers.remove(fw.getName()); + if(wit != null) { + //fw.getWorld().removeWorldAccess(wit); + } + worlds.remove(fw.getName()); + if(last_fworld == fw) { + last_world = null; + last_fworld = null; + } + } + */ + + private void initMetrics() { + /* + try { + Mod m = DynmapMod.class.getAnnotation(Mod.class); + metrics = new ForgeMetrics(m.name(), m.version()); + ; + ForgeMetrics.Graph features = metrics.createGraph("Features Used"); + + features.addPlotter(new ForgeMetrics.Plotter("Internal Web Server") { + @Override + public int getValue() { + if (!core.configuration.getBoolean("disable-webserver", false)) + return 1; + return 0; + } + }); + features.addPlotter(new ForgeMetrics.Plotter("Login Security") { + @Override + public int getValue() { + if(core.configuration.getBoolean("login-enabled", false)) + return 1; + return 0; + } + }); + features.addPlotter(new ForgeMetrics.Plotter("Player Info Protected") { + @Override + public int getValue() { + if(core.player_info_protected) + return 1; + return 0; + } + }); + + ForgeMetrics.Graph maps = metrics.createGraph("Map Data"); + maps.addPlotter(new ForgeMetrics.Plotter("Worlds") { + @Override + public int getValue() { + if(core.mapManager != null) + return core.mapManager.getWorlds().size(); + return 0; + } + }); + maps.addPlotter(new ForgeMetrics.Plotter("Maps") { + @Override + public int getValue() { + int cnt = 0; + if(core.mapManager != null) { + for(DynmapWorld w :core.mapManager.getWorlds()) { + cnt += w.maps.size(); + } + } + return cnt; + } + }); + maps.addPlotter(new ForgeMetrics.Plotter("HD Maps") { + @Override + public int getValue() { + int cnt = 0; + if(core.mapManager != null) { + for(DynmapWorld w :core.mapManager.getWorlds()) { + for(MapType mt : w.maps) { + if(mt instanceof HDMap) { + cnt++; + } + } + } + } + return cnt; + } + }); + for (String mod : modsused) { + features.addPlotter(new ForgeMetrics.Plotter(mod + " Blocks") { + @Override + public int getValue() { + return 1; + } + }); + } + + metrics.start(); + } catch (IOException e) { + // Failed to submit the stats :-( + } + */ + } + + private void saveWorlds() { + File f = new File(core.getDataFolder(), "forgeworlds.yml"); + ConfigurationNode cn = new ConfigurationNode(f); + ArrayList> lst = new ArrayList>(); + for(DynmapWorld fw : core.mapManager.getWorlds()) { + HashMap vals = new HashMap(); + vals.put("name", fw.getRawName()); + vals.put("height", fw.worldheight); + vals.put("sealevel", fw.sealevel); + vals.put("nether", fw.isNether()); + vals.put("the_end", ((ForgeWorld)fw).isTheEnd()); + vals.put("title", fw.getTitle()); + lst.add(vals); + } + cn.put("worlds", lst); + cn.put("isMCPC", isMCPC); + cn.put("useSaveFolderAsName", useSaveFolder); + cn.put("maxWorldHeight", ForgeWorld.getMaxWorldHeight()); + + cn.save(); + } + private void loadWorlds() { + isMCPC = server.getServerModName().contains("mcpc"); + File f = new File(core.getDataFolder(), "forgeworlds.yml"); + if(f.canRead() == false) { + useSaveFolder = true; + if (isMCPC) { + ForgeWorld.setMCPCMapping(); + } + else { + ForgeWorld.setSaveFolderMapping(); + } + return; + } + ConfigurationNode cn = new ConfigurationNode(f); + cn.load(); + // If defined, use maxWorldHeight + ForgeWorld.setMaxWorldHeight(cn.getInteger("maxWorldHeight", 256)); + + // If existing, only switch to save folder if MCPC+ + useSaveFolder = isMCPC; + // If setting defined, use it + if (cn.containsKey("useSaveFolderAsName")) { + useSaveFolder = cn.getBoolean("useSaveFolderAsName", useSaveFolder); + } + if (isMCPC) { + ForgeWorld.setMCPCMapping(); + } + else if (useSaveFolder) { + ForgeWorld.setSaveFolderMapping(); + } + // If inconsistent between MCPC and non-MCPC + if (isMCPC != cn.getBoolean("isMCPC", false)) { + return; + } + List> lst = cn.getMapList("worlds"); + if(lst == null) { + Log.warning("Discarding bad forgeworlds.yml"); + return; + } + + for(Map world : lst) { + try { + String name = (String)world.get("name"); + int height = (Integer)world.get("height"); + int sealevel = (Integer)world.get("sealevel"); + boolean nether = (Boolean)world.get("nether"); + boolean theend = (Boolean)world.get("the_end"); + String title = (String)world.get("title"); + if(name != null) { + ForgeWorld fw = new ForgeWorld(name, height, sealevel, nether, theend, title); + fw.setWorldUnloaded(); + core.processWorldLoad(fw); + worlds.put(fw.getName(), fw); + } + } catch (Exception x) { + Log.warning("Unable to load saved worlds from forgeworlds.yml"); + return; + } + } + } + public void serverStarted() { + this.onStart(); + if (core != null) { + core.serverStarted(); + } + } + public MinecraftServer getMCServer() { + return server; + } +} + +class DynmapCommandHandler +{ + private String cmd; + private DynmapPlugin plugin; + + public DynmapCommandHandler(String cmd, DynmapPlugin p) + { + this.cmd = cmd; + this.plugin = p; + } + + public void register(CommandDispatcher cd) { + cd.register(Commands.literal(cmd). + then(RequiredArgumentBuilder. argument("args", StringArgumentType.greedyString()). + executes((ctx) -> this.execute(plugin.getMCServer(), ctx.getSource(), ctx.getInput()))). + executes((ctx) -> this.execute(plugin.getMCServer(), ctx.getSource(), ctx.getInput()))); + } + +// @Override + public int execute(MinecraftServer server, CommandSource sender, + String cmdline) throws CommandException { + String[] args = cmdline.split("\\s+"); + plugin.onCommand(sender, cmd, Arrays.copyOfRange(args, 1, args.length)); + return 1; + } + +// @Override + public String getUsage(CommandSource arg0) { + return "Run /" + cmd + " help for details on using command"; + } +} + +class DynmapCommand extends DynmapCommandHandler { + DynmapCommand(DynmapPlugin p) { + super("dynmap", p); + } +} +class DmapCommand extends DynmapCommandHandler { + DmapCommand(DynmapPlugin p) { + super("dmap", p); + } +} +class DmarkerCommand extends DynmapCommandHandler { + DmarkerCommand(DynmapPlugin p) { + super("dmarker", p); + } +} +class DynmapExpCommand extends DynmapCommandHandler { + DynmapExpCommand(DynmapPlugin p) { + super("dynmapexp", p); + } +} + diff --git a/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/ForgeMapChunkCache.java b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/ForgeMapChunkCache.java new file mode 100644 index 00000000..2d6e1903 --- /dev/null +++ b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/ForgeMapChunkCache.java @@ -0,0 +1,1603 @@ +package org.dynmap.forge_1_14_4; + +import java.io.DataInputStream; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; + +import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.INBTBase; +import net.minecraft.nbt.NBTTagByte; +import net.minecraft.nbt.NBTTagByteArray; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagDouble; +import net.minecraft.nbt.NBTTagFloat; +import net.minecraft.nbt.NBTTagInt; +import net.minecraft.nbt.NBTTagIntArray; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.NBTTagLong; +import net.minecraft.nbt.NBTTagShort; +import net.minecraft.nbt.NBTTagString; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.World; +import net.minecraft.world.WorldServer; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.ChunkStatus; +import net.minecraft.world.chunk.IChunkProvider; +import net.minecraft.world.chunk.storage.AnvilChunkLoader; +import net.minecraft.world.chunk.storage.IChunkLoader; +import net.minecraft.world.chunk.storage.RegionFileCache; +import net.minecraft.world.gen.ChunkProviderServer; + +import org.dynmap.DynmapChunk; +import org.dynmap.DynmapCore; +import org.dynmap.DynmapWorld; +import org.dynmap.Log; +import org.dynmap.common.BiomeMap; +import org.dynmap.forge_1_14_4.SnapshotCache.SnapshotRec; +import org.dynmap.hdmap.HDBlockModels; +import org.dynmap.renderer.DynmapBlockState; +import org.dynmap.renderer.RenderPatchFactory; +import org.dynmap.utils.DynIntHashMap; +import org.dynmap.utils.MapChunkCache; +import org.dynmap.utils.MapIterator; +import org.dynmap.utils.BlockStep; +import org.dynmap.utils.VisibilityLimit; + +/** + * Container for managing chunks - dependent upon using chunk snapshots, since rendering is off server thread + */ +public class ForgeMapChunkCache extends MapChunkCache +{ + private static boolean init = false; + private static Field unloadqueue = null; + private static Field currentchunkloader = null; + private static Field updateEntityTick = null; + /* AnvilChunkLoader fields */ + private static Field chunksToRemove = null; // Map + //private static Field pendingAnvilChunksCoordinates = null; // Set + private static Method writechunktonbt = null; // writeChunkToNBT(Chunk c, World w, NBTTagCompound nbt) + + /* AnvilChunkLoaderPending fields */ + private static Field chunkCoord = null; + private static Field nbtTag = null; + + private World w; + private DynmapWorld dw; + private ChunkProviderServer cps; + private int nsect; + private List chunks; + private ListIterator iterator; + private int x_min, x_max, z_min, z_max; + private int x_dim; + private boolean biome, biomeraw, highesty, blockdata; + private HiddenChunkStyle hidestyle = HiddenChunkStyle.FILL_AIR; + private List visible_limits = null; + private List hidden_limits = null; + private boolean isempty = true; + private int snapcnt; + private ChunkSnapshot[] snaparray; /* Index = (x-x_min) + ((z-z_min)*x_dim) */ + private DynIntHashMap[] snaptile; + private byte[][] sameneighborbiomecnt; + private BiomeMap[][] biomemap; + private boolean[][] isSectionNotEmpty; /* Indexed by snapshot index, then by section index */ + private it.unimi.dsi.fastutil.longs.LongSet queue = null; + + private static final BlockStep unstep[] = { BlockStep.X_MINUS, BlockStep.Y_MINUS, BlockStep.Z_MINUS, + BlockStep.X_PLUS, BlockStep.Y_PLUS, BlockStep.Z_PLUS + }; + + private static BiomeMap[] biome_to_bmap; + + private static final int getIndexInChunk(int cx, int cy, int cz) { + return (cy << 8) | (cz << 4) | cx; + } + + /** + * Iterator for traversing map chunk cache (base is for non-snapshot) + */ + public class OurMapIterator implements MapIterator + { + private int x, y, z, chunkindex, bx, bz; + private ChunkSnapshot snap; + private BlockStep laststep; + private DynmapBlockState blk; + private final int worldheight; + private final int x_base; + private final int z_base; + + OurMapIterator(int x0, int y0, int z0) + { + x_base = x_min << 4; + z_base = z_min << 4; + + if (biome) + { + biomePrep(); + } + + initialize(x0, y0, z0); + worldheight = w.getHeight(); + } + @Override + public final void initialize(int x0, int y0, int z0) + { + this.x = x0; + this.y = y0; + this.z = z0; + this.chunkindex = ((x >> 4) - x_min) + (((z >> 4) - z_min) * x_dim); + this.bx = x & 0xF; + this.bz = z & 0xF; + + if((chunkindex >= snapcnt) || (chunkindex < 0)) { + snap = EMPTY; + } + else { + snap = snaparray[chunkindex]; + } + + laststep = BlockStep.Y_MINUS; + + if ((y >= 0) && (y < worldheight)) + { + blk = null; + } + else + { + blk = DynmapBlockState.AIR; + } + } + @Override + public int getBlockSkyLight() + { + try + { + return snap.getBlockSkyLight(bx, y, bz); + } + catch (ArrayIndexOutOfBoundsException aioobx) + { + return 15; + } + } + @Override + public final int getBlockEmittedLight() + { + try + { + return snap.getBlockEmittedLight(bx, y, bz); + } + catch (ArrayIndexOutOfBoundsException aioobx) + { + return 0; + } + } + private void biomePrep() + { + if (sameneighborbiomecnt != null) + { + return; + } + + int x_size = x_dim << 4; + int z_size = (z_max - z_min + 1) << 4; + sameneighborbiomecnt = new byte[x_size][]; + biomemap = new BiomeMap[x_size][]; + + for (int i = 0; i < x_size; i++) + { + sameneighborbiomecnt[i] = new byte[z_size]; + biomemap[i] = new BiomeMap[z_size]; + } + + for (int i = 0; i < x_size; i++) + { + for (int j = 0; j < z_size; j++) + { + if (j == 0) + initialize(i + x_base, 64, z_base); + else + stepPosition(BlockStep.Z_PLUS); + + int bb = snap.getBiome(bx, bz); + BiomeMap bm = BiomeMap.byBiomeID(bb); + + biomemap[i][j] = bm; + int cnt = 0; + + if (i > 0) + { + if (bm == biomemap[i - 1][j]) /* Same as one to left */ + { + cnt++; + sameneighborbiomecnt[i - 1][j]++; + } + + if ((j > 0) && (bm == biomemap[i - 1][j - 1])) + { + cnt++; + sameneighborbiomecnt[i - 1][j - 1]++; + } + + if ((j < (z_size - 1)) && (bm == biomemap[i - 1][j + 1])) + { + cnt++; + sameneighborbiomecnt[i - 1][j + 1]++; + } + } + + if ((j > 0) && (biomemap[i][j] == biomemap[i][j - 1])) /* Same as one to above */ + { + cnt++; + sameneighborbiomecnt[i][j - 1]++; + } + + sameneighborbiomecnt[i][j] = (byte)cnt; + } + } + } + @Override + public final BiomeMap getBiome() + { + try + { + return biomemap[x - x_base][z - z_base]; + } + catch (Exception ex) + { + return BiomeMap.NULL; + } + } + @Override + public final int getSmoothGrassColorMultiplier(int[] colormap) + { + int mult = 0xFFFFFF; + + try + { + int rx = x - x_base; + int rz = z - z_base; + BiomeMap bm = biomemap[rx][rz]; + + if (sameneighborbiomecnt[rx][rz] >= (byte)8) /* All neighbors same? */ + { + mult = bm.getModifiedGrassMultiplier(colormap[bm.biomeLookup()]); + } + else + { + int raccum = 0; + int gaccum = 0; + int baccum = 0; + + for (int xoff = -1; xoff < 2; xoff++) + { + for (int zoff = -1; zoff < 2; zoff++) + { + bm = biomemap[rx + xoff][rz + zoff]; + int rmult = bm.getModifiedGrassMultiplier(colormap[bm.biomeLookup()]); + raccum += (rmult >> 16) & 0xFF; + gaccum += (rmult >> 8) & 0xFF; + baccum += rmult & 0xFF; + } + } + + mult = ((raccum / 9) << 16) | ((gaccum / 9) << 8) | (baccum / 9); + } + } + catch (Exception x) + { + mult = 0xFFFFFF; + } + + return mult; + } + @Override + public final int getSmoothFoliageColorMultiplier(int[] colormap) + { + int mult = 0xFFFFFF; + + try + { + int rx = x - x_base; + int rz = z - z_base; + BiomeMap bm = biomemap[rx][rz]; + + if (sameneighborbiomecnt[rx][rz] >= (byte)8) /* All neighbors same? */ + { + mult = bm.getModifiedFoliageMultiplier(colormap[bm.biomeLookup()]); + } + else + { + int raccum = 0; + int gaccum = 0; + int baccum = 0; + + for (int xoff = -1; xoff < 2; xoff++) + { + for (int zoff = -1; zoff < 2; zoff++) + { + bm = biomemap[rx + xoff][rz + zoff]; + int rmult = bm.getModifiedFoliageMultiplier(colormap[bm.biomeLookup()]); + raccum += (rmult >> 16) & 0xFF; + gaccum += (rmult >> 8) & 0xFF; + baccum += rmult & 0xFF; + } + } + + mult = ((raccum / 9) << 16) | ((gaccum / 9) << 8) | (baccum / 9); + } + } + catch (Exception x) + { + mult = 0xFFFFFF; + } + + return mult; + } + @Override + public final int getSmoothColorMultiplier(int[] colormap, int[] swampmap) + { + int mult = 0xFFFFFF; + + try + { + int rx = x - x_base; + int rz = z - z_base; + BiomeMap bm = biomemap[rx][rz]; + + if (sameneighborbiomecnt[rx][rz] >= (byte)8) /* All neighbors same? */ + { + if (bm == BiomeMap.SWAMPLAND) + { + mult = swampmap[bm.biomeLookup()]; + } + else + { + mult = colormap[bm.biomeLookup()]; + } + } + else + { + int raccum = 0; + int gaccum = 0; + int baccum = 0; + + for (int xoff = -1; xoff < 2; xoff++) + { + for (int zoff = -1; zoff < 2; zoff++) + { + bm = biomemap[rx + xoff][rz + zoff]; + int rmult; + + if (bm == BiomeMap.SWAMPLAND) + { + rmult = swampmap[bm.biomeLookup()]; + } + else + { + rmult = colormap[bm.biomeLookup()]; + } + + raccum += (rmult >> 16) & 0xFF; + gaccum += (rmult >> 8) & 0xFF; + baccum += rmult & 0xFF; + } + } + + mult = ((raccum / 9) << 16) | ((gaccum / 9) << 8) | (baccum / 9); + } + } + catch (Exception x) + { + mult = 0xFFFFFF; + } + + return mult; + } + @Override + public final int getSmoothWaterColorMultiplier() + { + try + { + int rx = x - x_base; + int rz = z - z_base; + BiomeMap bm = biomemap[rx][rz]; + + if (sameneighborbiomecnt[rx][rz] >= (byte)8) /* All neighbors same? */ + { + return bm.getWaterColorMult(); + } + + int raccum = 0; + int gaccum = 0; + int baccum = 0; + + for (int xoff = -1; xoff < 2; xoff++) + { + for (int zoff = -1; zoff < 2; zoff++) + { + bm = biomemap[rx + xoff][rz + zoff]; + int mult = bm.getWaterColorMult(); + raccum += (mult >> 16) & 0xFF; + gaccum += (mult >> 8) & 0xFF; + baccum += mult & 0xFF; + } + } + + return ((raccum / 9) << 16) | ((gaccum / 9) << 8) | (baccum / 9); + } + catch (Exception x) + { + return 0xFFFFFF; + } + } + @Override + public final int getSmoothWaterColorMultiplier(int[] colormap) + { + int mult = 0xFFFFFF; + + try + { + int rx = x - x_base; + int rz = z - z_base; + BiomeMap bm = biomemap[rx][rz]; + + if (sameneighborbiomecnt[rx][rz] >= (byte)8) /* All neighbors same? */ + { + mult = colormap[bm.biomeLookup()]; + } + else + { + int raccum = 0; + int gaccum = 0; + int baccum = 0; + + for (int xoff = -1; xoff < 2; xoff++) + { + for (int zoff = -1; zoff < 2; zoff++) + { + bm = biomemap[rx + xoff][rz + zoff]; + int rmult = colormap[bm.biomeLookup()]; + raccum += (rmult >> 16) & 0xFF; + gaccum += (rmult >> 8) & 0xFF; + baccum += rmult & 0xFF; + } + } + + mult = ((raccum / 9) << 16) | ((gaccum / 9) << 8) | (baccum / 9); + } + } + catch (Exception x) + { + mult = 0xFFFFFF; + } + + return mult; + } + /** + * Step current position in given direction + */ + @Override + public final void stepPosition(BlockStep step) + { + blk = null; + + switch (step.ordinal()) + { + case 0: + x++; + bx++; + + if (bx == 16) /* Next chunk? */ + { + bx = 0; + chunkindex++; + if((chunkindex >= snapcnt) || (chunkindex < 0)) { + snap = EMPTY; + } + else { + snap = snaparray[chunkindex]; + } + } + + break; + + case 1: + y++; + + if (y >= worldheight) + { + blk = DynmapBlockState.AIR; + } + + break; + + case 2: + z++; + bz++; + + if (bz == 16) /* Next chunk? */ + { + bz = 0; + chunkindex += x_dim; + if((chunkindex >= snapcnt) || (chunkindex < 0)) { + snap = EMPTY; + } + else { + snap = snaparray[chunkindex]; + } + } + break; + + case 3: + x--; + bx--; + + if (bx == -1) /* Next chunk? */ + { + bx = 15; + chunkindex--; + if((chunkindex >= snapcnt) || (chunkindex < 0)) { + snap = EMPTY; + } + else { + snap = snaparray[chunkindex]; + } + } + + break; + + case 4: + y--; + + if (y < 0) + { + blk = DynmapBlockState.AIR; + } + + break; + + case 5: + z--; + bz--; + + if (bz == -1) /* Next chunk? */ + { + bz = 15; + chunkindex -= x_dim; + if((chunkindex >= snapcnt) || (chunkindex < 0)) { + snap = EMPTY; + } + else { + snap = snaparray[chunkindex]; + } + } + break; + } + + laststep = step; + } + /** + * Unstep current position to previous position + */ + @Override + public BlockStep unstepPosition() + { + BlockStep ls = laststep; + stepPosition(unstep[ls.ordinal()]); + return ls; + } + /** + * Unstep current position in oppisite director of given step + */ + @Override + public void unstepPosition(BlockStep s) + { + stepPosition(unstep[s.ordinal()]); + } + @Override + public final void setY(int y) + { + if (y > this.y) + { + laststep = BlockStep.Y_PLUS; + } + else + { + laststep = BlockStep.Y_MINUS; + } + + this.y = y; + + if ((y < 0) || (y >= worldheight)) + { + blk = DynmapBlockState.AIR; + } + else + { + blk = null; + } + } + @Override + public final int getX() + { + return x; + } + @Override + public final int getY() + { + return y; + } + @Override + public final int getZ() + { + return z; + } + @Override + public final DynmapBlockState getBlockTypeAt(BlockStep s) + { + if (s == BlockStep.Y_MINUS) + { + if (y > 0) + { + return snap.getBlockType(bx, y - 1, bz); + } + } + else if (s == BlockStep.Y_PLUS) + { + if (y < (worldheight - 1)) + { + return snap.getBlockType(bx, y + 1, bz); + } + } + else + { + BlockStep ls = laststep; + stepPosition(s); + DynmapBlockState tid = snap.getBlockType(bx, y, bz); + unstepPosition(); + laststep = ls; + return tid; + } + + return DynmapBlockState.AIR; + } + @Override + public BlockStep getLastStep() + { + return laststep; + } + @Override + public int getWorldHeight() + { + return worldheight; + } + @Override + public long getBlockKey() + { + return (((chunkindex * worldheight) + y) << 8) | (bx << 4) | bz; + } + @Override + public final boolean isEmptySection() + { + try + { + return !isSectionNotEmpty[chunkindex][y >> 4]; + } + catch (Exception x) + { + initSectionData(chunkindex); + return !isSectionNotEmpty[chunkindex][y >> 4]; + } + } + @Override + public RenderPatchFactory getPatchFactory() { + return HDBlockModels.getPatchDefinitionFactory(); + } + @Override + public Object getBlockTileEntityField(String fieldId) { + try { + int idx = getIndexInChunk(bx,y,bz); + Object[] vals = (Object[])snaptile[chunkindex].get(idx); + for (int i = 0; i < vals.length; i += 2) { + if (vals[i].equals(fieldId)) { + return vals[i+1]; + } + } + } catch (Exception x) { + } + return null; + } + @Override + public DynmapBlockState getBlockTypeAt(int xoff, int yoff, int zoff) { + int xx = this.x + xoff; + int yy = this.y + yoff; + int zz = this.z + zoff; + int idx = ((xx >> 4) - x_min) + (((zz >> 4) - z_min) * x_dim); + try { + return snaparray[idx].getBlockType(xx & 0xF, yy, zz & 0xF); + } catch (Exception x) { + return DynmapBlockState.AIR; + } + } + @Override + public Object getBlockTileEntityFieldAt(String fieldId, int xoff, + int yoff, int zoff) { + return null; + } + @Override + public long getInhabitedTicks() { + try { + return snap.getInhabitedTicks(); + } catch (Exception x) { + return 0; + } + } + @Override + public DynmapBlockState getBlockType() { + if (blk == null) { + blk = snap.getBlockType(bx, y, bz); + } + return blk; + } + } + + private class OurEndMapIterator extends OurMapIterator + { + OurEndMapIterator(int x0, int y0, int z0) + { + super(x0, y0, z0); + } + @Override + public final int getBlockSkyLight() + { + return 15; + } + } + /** + * Chunk cache for representing unloaded chunk (or air) + */ + private static class EmptyChunk extends ChunkSnapshot + { + public EmptyChunk() + { + super(256, 0, 0, 0, 0); + } + /* Need these for interface, but not used */ + @Override + public int getX() + { + return 0; + } + @Override + public int getZ() + { + return 0; + } + @Override + public final DynmapBlockState getBlockType(int x, int y, int z) + { + return DynmapBlockState.AIR; + } + @Override + public final int getBlockSkyLight(int x, int y, int z) + { + return 15; + } + @Override + public final int getBlockEmittedLight(int x, int y, int z) + { + return 0; + } + @Override + public final int getHighestBlockYAt(int x, int z) + { + return 0; + } + @Override + public int getBiome(int x, int z) + { + return -1; + } + @Override + public boolean isSectionEmpty(int sy) + { + return true; + } + } + + /** + * Chunk cache for representing generic stone chunk + */ + private static class PlainChunk extends ChunkSnapshot + { + private DynmapBlockState fill; + + PlainChunk(String fill) + { + super(256, 0, 0, 0, 0); + this.fill = DynmapBlockState.getBaseStateByName(fill); + } + /* Need these for interface, but not used */ + @Override + public int getX() + { + return 0; + } + @Override + public int getZ() + { + return 0; + } + @Override + public int getBiome(int x, int z) + { + return -1; + } + @Override + public final DynmapBlockState getBlockType(int x, int y, int z) + { + if (y < 64) + { + return fill; + } + + return DynmapBlockState.AIR; + } + @Override + public final int getBlockSkyLight(int x, int y, int z) + { + if (y < 64) + { + return 0; + } + + return 15; + } + @Override + public final int getBlockEmittedLight(int x, int y, int z) + { + return 0; + } + @Override + public final int getHighestBlockYAt(int x, int z) + { + return 64; + } + @Override + public boolean isSectionEmpty(int sy) + { + return (sy < 4); + } + } + + private static final EmptyChunk EMPTY = new EmptyChunk(); + private static final PlainChunk STONE = new PlainChunk(DynmapBlockState.STONE_BLOCK); + private static final PlainChunk OCEAN = new PlainChunk(DynmapBlockState.WATER_BLOCK); + + + public static void init() { + if (!init) + { + Field[] f = ChunkProviderServer.class.getDeclaredFields(); + + for(int i = 0; i < f.length; i++) { + if((unloadqueue == null) && f[i].getType().isAssignableFrom(it.unimi.dsi.fastutil.longs.LongSet.class)) { + unloadqueue = f[i]; + //Log.info("Found unloadqueue - " + f[i].getName()); + unloadqueue.setAccessible(true); + } + else if((currentchunkloader == null) && f[i].getType().isAssignableFrom(IChunkLoader.class)) { + currentchunkloader = f[i]; + //Log.info("Found currentchunkprovider - " + f[i].getName()); + currentchunkloader.setAccessible(true); + } + } + + f = WorldServer.class.getDeclaredFields(); + for(int i = 0; i < f.length; i++) { + if((updateEntityTick == null) && f[i].getType().isAssignableFrom(int.class)) { + updateEntityTick = f[i]; + //Log.info("Found updateEntityTick - " + f[i].getName()); + updateEntityTick.setAccessible(true); + } + } + + f = AnvilChunkLoader.class.getDeclaredFields(); + for(int i = 0; i < f.length; i++) { + if((chunksToRemove == null) && (f[i].getType().equals(Map.class))) { + chunksToRemove = f[i]; + //Log.info("Found chunksToRemove - " + f[i].getName()); + chunksToRemove.setAccessible(true); + } +// else if((pendingAnvilChunksCoordinates == null) && (f[i].getType().equals(it.unimi.dsi.fastutil.longs.LongSet.class))) { +// //Log.info("Found pendingAnvilChunksCoordinates - " + f[i].getName()); +// pendingAnvilChunksCoordinates = f[i]; +// pendingAnvilChunksCoordinates.setAccessible(true); +// } + } + // Get writeChunkToNBT method + Method[] ma = AnvilChunkLoader.class.getDeclaredMethods(); + for (Method m : ma) { + Class[] p = m.getParameterTypes(); + if ((p.length == 3) && (p[0].equals(Chunk.class)) && (p[1].equals(World.class)) && (p[2].equals(NBTTagCompound.class))) { + writechunktonbt = m; + Log.info("Found writechunktonbt- " + m.getName()); + m.setAccessible(true); + break; + } + } + + if ((unloadqueue == null) || (currentchunkloader == null) || (writechunktonbt == null)) + { + Log.severe("ERROR: cannot find unload queue or chunk provider field - dynmap cannot load chunks"); + } + if (updateEntityTick == null) { + Log.severe("ERROR: cannot find updateEntityTick - dynmap cannot drive entity cleanup when no players are active"); + } + + init = true; + } + } + + /** + * Construct empty cache + */ + public ForgeMapChunkCache() + { + init(); + } + + public void setChunks(ForgeWorld dw, List chunks) + { + this.dw = dw; + this.w = dw.getWorld(); + if(dw.isLoaded()) { + /* Check if world's provider is ChunkProviderServer */ + IChunkProvider cp = this.w.getChunkProvider(); + + if (cp instanceof ChunkProviderServer) + { + cps = (ChunkProviderServer)cp; + } + else + { + Log.severe("Error: world " + dw.getName() + " has unsupported chunk provider"); + } + } + else { + chunks = new ArrayList(); + } + nsect = dw.worldheight >> 4; + this.chunks = chunks; + + /* Compute range */ + if (chunks.size() == 0) + { + this.x_min = 0; + this.x_max = 0; + this.z_min = 0; + this.z_max = 0; + x_dim = 1; + } + else + { + x_min = x_max = chunks.get(0).x; + z_min = z_max = chunks.get(0).z; + + for (DynmapChunk c : chunks) + { + if (c.x > x_max) + { + x_max = c.x; + } + + if (c.x < x_min) + { + x_min = c.x; + } + + if (c.z > z_max) + { + z_max = c.z; + } + + if (c.z < z_min) + { + z_min = c.z; + } + } + + x_dim = x_max - x_min + 1; + } + + snapcnt = x_dim * (z_max-z_min+1); + snaparray = new ChunkSnapshot[snapcnt]; + snaptile = new DynIntHashMap[snapcnt]; + isSectionNotEmpty = new boolean[snapcnt][]; + + try + { + if ((unloadqueue != null) && (cps != null)) + { + queue = (it.unimi.dsi.fastutil.longs.LongSet) unloadqueue.get(cps); + } + } + catch (IllegalArgumentException iax) + { + } + catch (IllegalAccessException e) + { + } + + } + + private static boolean didError = false; + + public NBTTagCompound readChunk(int x, int z) { + if((cps == null) || (!(cps.chunkLoader instanceof AnvilChunkLoader)) || + (((chunksToRemove == null) /*|| (pendingAnvilChunksCoordinates == null) */))) { + if (!didError) { + Log.severe("**** DYNMAP CANNOT READ CHUNKS (UNSUPPORTED CHUNK LOADER) ****"); + didError = true; + } + return null; + } + try { + AnvilChunkLoader acl = (AnvilChunkLoader)cps.chunkLoader; + Map chunkstoremove = null; + //it.unimi.dsi.fastutil.longs.LongSet pendingcoords; + + chunkstoremove = (Map)chunksToRemove.get(acl); + //pendingcoords = (it.unimi.dsi.fastutil.longs.LongSet) pendingAnvilChunksCoordinates.get(acl); + + NBTTagCompound rslt = null; + ChunkPos coord = new ChunkPos(x, z); + + //} + // if (pendingcoords.contains(coord.asLong()) { + // for (Object o : chunkstoremove.values()) { + // if (chunkCoord == null) { + // Field[] f = o.getClass().getDeclaredFields(); + // for(Field ff : f) { + // if((chunkCoord == null) && (ff.getType().equals(ChunkPos.class))) { + // chunkCoord = ff; + // chunkCoord.setAccessible(true); + // } + // else if((nbtTag == null) && (ff.getType().equals(NBTTagCompound.class))) { + // nbtTag = ff; + // nbtTag.setAccessible(true); + // } + // } + // if ((chunkCoord == null) || (nbtTag == null)) { + // Log.severe("Error getting chunkCoord and nbtTag for Forge"); + // return null; + // } + // } + // ChunkPos occ = (ChunkPos)chunkCoord.get(o); + + // if (occ.equals(coord)) { + // rslt = (NBTTagCompound)nbtTag.get(o); + // break; + // } + // } + // } + + if (rslt == null) { + DataInputStream str = RegionFileCache.getChunkInputStream(acl.chunkSaveLocation, x, z); + + if (str == null) { + return null; + } + rslt = CompressedStreamTools.read(str); + } + if(rslt != null) { + rslt = rslt.getCompound("Level"); + // Don't load uncooked chunks + String stat = rslt.getString("Status"); + ChunkStatus cs = ChunkStatus.getByName(stat); + if ((stat == null) || + // Needs to be at least lighted + (!cs.isAtLeast(ChunkStatus.LIGHTED))) { + rslt = null; + } + } + //Log.info(String.format("loadChunk(%d,%d)=%s", x, z, (rslt != null) ? rslt.toString() : "null")); + return rslt; + } catch (Exception exc) { + Log.severe(String.format("Error reading chunk: %s,%d,%d", dw.getName(), x, z), exc); + return null; + } + } + + private Object getNBTValue(INBTBase v) { + Object val = null; + switch(v.getId()) { + case 1: // Byte + val = Byte.valueOf(((NBTTagByte)v).getByte()); + break; + case 2: // Short + val = Short.valueOf(((NBTTagShort)v).getShort()); + break; + case 3: // Int + val = Integer.valueOf(((NBTTagInt)v).getInt()); + break; + case 4: // Long + val = Long.valueOf(((NBTTagLong)v).getLong()); + break; + case 5: // Float + val = Float.valueOf(((NBTTagFloat)v).getFloat()); + break; + case 6: // Double + val = Double.valueOf(((NBTTagDouble)v).getDouble()); + break; + case 7: // Byte[] + val = ((NBTTagByteArray)v).getByteArray(); + break; + case 8: // String + val = ((NBTTagString)v).getString(); + break; + case 9: // List + NBTTagList tl = (NBTTagList) v; + ArrayList vlist = new ArrayList(); + int type = tl.getTagType(); + for (int i = 0; i < tl.size(); i++) { + switch (type) { + case 5: + float fv = tl.getFloat(i); + vlist.add(fv); + break; + case 6: + double dv = tl.getDouble(i); + vlist.add(dv); + break; + case 8: + String sv = tl.getString(i); + vlist.add(sv); + break; + case 10: + NBTTagCompound tc = tl.getCompound(i); + vlist.add(getNBTValue(tc)); + break; + case 11: + int[] ia = tl.getIntArray(i); + vlist.add(ia); + break; + } + } + val = vlist; + break; + case 10: // Map + NBTTagCompound tc = (NBTTagCompound) v; + HashMap vmap = new HashMap(); + for (Object t : tc.keySet()) { + String st = (String) t; + INBTBase tg = tc.get(st); + vmap.put(st, getNBTValue(tg)); + } + val = vmap; + break; + case 11: // Int[] + val = ((NBTTagIntArray)v).getIntArray(); + break; + } + return val; + } + + private boolean isChunkVisible(DynmapChunk chunk) { + boolean vis = true; + if(visible_limits != null) { + vis = false; + for(VisibilityLimit limit : visible_limits) { + if (limit.doIntersectChunk(chunk.x, chunk.z)) { + vis = true; + break; + } + } + } + if(vis && (hidden_limits != null)) { + for(VisibilityLimit limit : hidden_limits) { + if (limit.doIntersectChunk(chunk.x, chunk.z)) { + vis = false; + break; + } + } + } + return vis; + } + + private boolean tryChunkCache(DynmapChunk chunk, boolean vis) { + /* Check if cached chunk snapshot found */ + ChunkSnapshot ss = null; + SnapshotRec ssr = DynmapPlugin.plugin.sscache.getSnapshot(dw.getName(), chunk.x, chunk.z, blockdata, biome, biomeraw, highesty); + if(ssr != null) { + ss = ssr.ss; + if (!vis) + { + if (hidestyle == HiddenChunkStyle.FILL_STONE_PLAIN) + { + ss = STONE; + } + else if (hidestyle == HiddenChunkStyle.FILL_OCEAN) + { + ss = OCEAN; + } + else + { + ss = EMPTY; + } + } + int idx = (chunk.x-x_min) + (chunk.z - z_min)*x_dim; + snaparray[idx] = ss; + snaptile[idx] = ssr.tileData; + } + return (ssr != null); + } + + private boolean isChunkUnloadPending(DynmapChunk chunk) { + boolean isunloadpending = false; + + if (queue != null) + { + long coord = ChunkPos.asLong(chunk.x, chunk.z); + isunloadpending = queue.contains(coord); + } + return isunloadpending; + } + + // Prep snapshot and add to cache + private SnapshotRec prepChunkSnapshot(DynmapChunk chunk, NBTTagCompound nbt) { + ChunkSnapshot ss = new ChunkSnapshot(nbt, dw.worldheight); + DynIntHashMap tileData = new DynIntHashMap(); + + NBTTagList tiles = nbt.getList("TileEntities", 10); + if(tiles == null) tiles = new NBTTagList(); + /* Get tile entity data */ + List vals = new ArrayList(); + for(int tid = 0; tid < tiles.size(); tid++) { + NBTTagCompound tc = tiles.getCompound(tid); + int tx = tc.getInt("x"); + int ty = tc.getInt("y"); + int tz = tc.getInt("z"); + int cx = tx & 0xF; + int cz = tz & 0xF; + DynmapBlockState blk = ss.getBlockType(cx, ty, cz); + String[] te_fields = HDBlockModels.getTileEntityFieldsNeeded(blk); + if(te_fields != null) { + vals.clear(); + for(String id: te_fields) { + INBTBase v = tc.get(id); /* Get field */ + if(v != null) { + Object val = getNBTValue(v); + if(val != null) { + vals.add(id); + vals.add(val); + } + } + } + if(vals.size() > 0) { + Object[] vlist = vals.toArray(new Object[vals.size()]); + tileData.put(getIndexInChunk(cx, ty, cz), vlist); + } + } + } + SnapshotRec ssr = new SnapshotRec(); + ssr.ss = ss; + ssr.tileData = tileData; + DynmapPlugin.plugin.sscache.putSnapshot(dw.getName(), chunk.x, chunk.z, ssr, blockdata, biome, biomeraw, highesty); + + return ssr; + } + + /** + * Read NBT data from loaded chunks - needs to be called from server/world thread to be safe + * @returns number loaded + */ + public int getLoadedChunks() { + int cnt = 0; + if(!dw.isLoaded()) { + isempty = true; + unloadChunks(); + return 0; + } + ListIterator iter = chunks.listIterator(); + while (iter.hasNext()) { + long startTime = System.nanoTime(); + DynmapChunk chunk = iter.next(); + int chunkindex = (chunk.x-x_min) + (chunk.z - z_min)*x_dim; + if (snaparray[chunkindex] != null) continue; // Skip if already processed + + boolean vis = isChunkVisible(chunk); + + /* Check if cached chunk snapshot found */ + if (tryChunkCache(chunk, vis)) { + endChunkLoad(startTime, ChunkStats.CACHED_SNAPSHOT_HIT); + cnt++; + } + // If chunk is loaded and not being unloaded, we're grabbing its NBT data + else if (cps.chunkExists(chunk.x, chunk.z) && (!isChunkUnloadPending(chunk))) { + ChunkSnapshot ss; + DynIntHashMap tileData; + if (vis) { // If visible + NBTTagCompound nbt = new NBTTagCompound(); + try { + writechunktonbt.invoke(cps.chunkLoader, cps.getChunk(chunk.x, chunk.z, false, false), w, nbt); + } catch (IllegalAccessException e) { + } catch (IllegalArgumentException e) { + } catch (InvocationTargetException e) { + } + SnapshotRec ssr = prepChunkSnapshot(chunk, nbt); + ss = ssr.ss; + tileData = ssr.tileData; + } + else { + if (hidestyle == HiddenChunkStyle.FILL_STONE_PLAIN) { + ss = STONE; + } + else if (hidestyle == HiddenChunkStyle.FILL_OCEAN) { + ss = OCEAN; + } + else { + ss = EMPTY; + } + tileData = new DynIntHashMap(); + } + snaparray[chunkindex] = ss; + snaptile[chunkindex] = tileData; + endChunkLoad(startTime, ChunkStats.LOADED_CHUNKS); + cnt++; + } + } + return cnt; + } + + @Override + public int loadChunks(int max_to_load) + { + return getLoadedChunks() + readChunks(max_to_load); + + } + + public int readChunks(int max_to_load) + { + if(!dw.isLoaded()) { + isempty = true; + unloadChunks(); + return 0; + } + + int cnt = 0; + + if (iterator == null) + { + iterator = chunks.listIterator(); + } + + DynmapCore.setIgnoreChunkLoads(true); + + // Load the required chunks. + while ((cnt < max_to_load) && iterator.hasNext()) + { + long startTime = System.nanoTime(); + + DynmapChunk chunk = iterator.next(); + + int chunkindex = (chunk.x-x_min) + (chunk.z - z_min)*x_dim; + + if (snaparray[chunkindex] != null) continue; // Skip if already processed + + boolean vis = isChunkVisible(chunk); + + /* Check if cached chunk snapshot found */ + if (tryChunkCache(chunk, vis)) { + endChunkLoad(startTime, ChunkStats.CACHED_SNAPSHOT_HIT); + } + else { + NBTTagCompound nbt = readChunk(chunk.x, chunk.z); + // If read was good + if (nbt != null) { + ChunkSnapshot ss; + DynIntHashMap tileData; + // If hidden + if (!vis) { + if (hidestyle == HiddenChunkStyle.FILL_STONE_PLAIN) { + ss = STONE; + } + else if (hidestyle == HiddenChunkStyle.FILL_OCEAN) { + ss = OCEAN; + } + else { + ss = EMPTY; + } + tileData = new DynIntHashMap(); + } + else { + // Prep snapshot + SnapshotRec ssr = prepChunkSnapshot(chunk, nbt); + ss = ssr.ss; + tileData = ssr.tileData; + } + snaparray[chunkindex] = ss; + snaptile[chunkindex] = tileData; + endChunkLoad(startTime, ChunkStats.UNLOADED_CHUNKS); + } + else { + endChunkLoad(startTime, ChunkStats.UNGENERATED_CHUNKS); + } + } + cnt++; + } + + DynmapCore.setIgnoreChunkLoads(false); + + if (iterator.hasNext() == false) /* If we're done */ + { + isempty = true; + + /* Fill missing chunks with empty dummy chunk */ + for (int i = 0; i < snaparray.length; i++) + { + if (snaparray[i] == null) + { + snaparray[i] = EMPTY; + } + else if (snaparray[i] != EMPTY) + { + isempty = false; + } + } + } + return cnt; + } + /** + * Test if done loading + */ + public boolean isDoneLoading() + { + if(!dw.isLoaded()) { + return true; + } + if (iterator != null) + { + return !iterator.hasNext(); + } + + return false; + } + /** + * Test if all empty blocks + */ + public boolean isEmpty() + { + return isempty; + } + /** + * Unload chunks + */ + public void unloadChunks() + { + if (snaparray != null) + { + for (int i = 0; i < snaparray.length; i++) + { + snaparray[i] = null; + } + + snaparray = null; + } + } + private void initSectionData(int idx) + { + isSectionNotEmpty[idx] = new boolean[nsect + 1]; + + if (snaparray[idx] != EMPTY) + { + for (int i = 0; i < nsect; i++) + { + if (snaparray[idx].isSectionEmpty(i) == false) + { + isSectionNotEmpty[idx][i] = true; + } + } + } + } + public boolean isEmptySection(int sx, int sy, int sz) + { + int idx = (sx - x_min) + (sz - z_min) * x_dim; + + if (isSectionNotEmpty[idx] == null) + { + initSectionData(idx); + } + + return !isSectionNotEmpty[idx][sy]; + } + + /** + * Get cache iterator + */ + public MapIterator getIterator(int x, int y, int z) + { + if (dw.getEnvironment().equals("the_end")) + { + return new OurEndMapIterator(x, y, z); + } + + return new OurMapIterator(x, y, z); + } + /** + * Set hidden chunk style (default is FILL_AIR) + */ + public void setHiddenFillStyle(HiddenChunkStyle style) + { + this.hidestyle = style; + } + /** + * Add visible area limit - can be called more than once + * Needs to be set before chunks are loaded + * Coordinates are block coordinates + */ + public void setVisibleRange(VisibilityLimit lim) { + if(visible_limits == null) + visible_limits = new ArrayList(); + visible_limits.add(lim); + } + /** + * Add hidden area limit - can be called more than once + * Needs to be set before chunks are loaded + * Coordinates are block coordinates + */ + public void setHiddenRange(VisibilityLimit lim) { + if(hidden_limits == null) + hidden_limits = new ArrayList(); + hidden_limits.add(lim); + } + @Override + public boolean setChunkDataTypes(boolean blockdata, boolean biome, boolean highestblocky, boolean rawbiome) + { + this.biome = biome; + this.biomeraw = rawbiome; + this.highesty = highestblocky; + this.blockdata = blockdata; + return true; + } + @Override + public DynmapWorld getWorld() + { + return dw; + } + + static + { + Biome b[] = DynmapPlugin.getBiomeList(); + BiomeMap[] bm = BiomeMap.values(); + biome_to_bmap = new BiomeMap[256]; + + for (int i = 0; i < biome_to_bmap.length; i++) + { + biome_to_bmap[i] = BiomeMap.NULL; + } + + for (int i = 0; i < b.length; i++) + { + if(b[i] == null) continue; + + String bs = b[i].getTranslationKey(); + + for (int j = 0; j < bm.length; j++) + { + if (bm[j].toString().equals(bs)) + { + biome_to_bmap[i] = bm[j]; + break; + } + } + } + } +} diff --git a/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/ForgeWorld.java b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/ForgeWorld.java new file mode 100644 index 00000000..7307555b --- /dev/null +++ b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/ForgeWorld.java @@ -0,0 +1,261 @@ +package org.dynmap.forge_1_14_4; +/** + * Forge specific implementation of DynmapWorld + */ +import java.util.List; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.dimension.Dimension; +import net.minecraft.world.dimension.DimensionType; +import net.minecraft.world.dimension.EndDimension; +import net.minecraft.world.dimension.NetherDimension; +import net.minecraft.world.gen.Heightmap.Type; +import net.minecraft.world.EnumLightType; +import net.minecraft.world.IWorld; +import net.minecraft.world.World; +import net.minecraft.world.border.WorldBorder; + +import org.dynmap.DynmapChunk; +import org.dynmap.DynmapLocation; +import org.dynmap.DynmapWorld; +import org.dynmap.utils.MapChunkCache; +import org.dynmap.utils.Polygon; + +public class ForgeWorld extends DynmapWorld +{ + private IWorld world; + private final boolean skylight; + private final boolean isnether; + private final boolean istheend; + private final String env; + private DynmapLocation spawnloc = new DynmapLocation(); + private static boolean doMCPCMapping = false; + private static boolean doSaveFolderMapping = false; + private static int maxWorldHeight = 256; // Maximum allows world height + + public static void setMCPCMapping() { + doMCPCMapping = true; + } + public static void setSaveFolderMapping() { + doSaveFolderMapping = true; + } + public static int getMaxWorldHeight() { + return maxWorldHeight; + } + public static void setMaxWorldHeight(int h) { + maxWorldHeight = h; + } + + public static String getWorldName(IWorld w) { + String n; + if (doMCPCMapping) { // MCPC+ mapping + n = w.getWorldInfo().getWorldName(); + } + else if (doSaveFolderMapping) { // New vanilla Forge mapping (consistent with MCPC+) + if (w.getDimension().getType() == DimensionType.OVERWORLD) { + n = w.getWorldInfo().getWorldName(); + } + else { + n = "DIM" + w.getDimension().getType().getId(); + } + } + else { // Legacy mapping + n = w.getWorldInfo().getWorldName(); + Dimension wp = w.getDimension(); + switch(wp.getType().getId()) { + case 0: + break; + case -1: + n += "_nether"; + break; + case 1: + n += "_the_end"; + break; + default: + n += "_" + wp.getType().getId(); + break; + } + } + return n; + } + + public ForgeWorld(IWorld w) + { + this(getWorldName(w), w.getWorld().getHeight(), w.getSeaLevel(), w.getDimension() instanceof NetherDimension, + w.getDimension() instanceof EndDimension, + w.getWorldInfo().getWorldName() + "/" + w.getDimension().getType().toString()); + setWorldLoaded(w); + } + public ForgeWorld(String name, int height, int sealevel, boolean nether, boolean the_end, String deftitle) + { + super(name, (height > maxWorldHeight)?maxWorldHeight:height, sealevel); + world = null; + setTitle(deftitle); + isnether = nether; + istheend = the_end; + skylight = !(isnether || istheend); + + if (isnether) + { + env = "nether"; + } + else if (istheend) + { + env = "the_end"; + } + else + { + env = "normal"; + } + + } + /* Test if world is nether */ + @Override + public boolean isNether() + { + return isnether; + } + public boolean isTheEnd() + { + return istheend; + } + /* Get world spawn location */ + @Override + public DynmapLocation getSpawnLocation() + { + if(world != null) { + BlockPos sloc = world.getSpawnPoint(); + spawnloc.x = sloc.getX(); + spawnloc.y = sloc.getY(); + spawnloc.z = sloc.getZ(); + spawnloc.world = this.getName(); + } + return spawnloc; + } + /* Get world time */ + @Override + public long getTime() + { + if(world != null) + return world.getWorld().getGameTime(); + else + return -1; + } + /* World is storming */ + @Override + public boolean hasStorm() + { + if(world != null) + return world.getWorld().isRaining(); + else + return false; + } + /* World is thundering */ + @Override + public boolean isThundering() + { + if(world != null) + return world.getWorld().isThundering(); + else + return false; + } + /* World is loaded */ + @Override + public boolean isLoaded() + { + return (world != null); + } + /* Set world to unloaded */ + @Override + public void setWorldUnloaded() + { + getSpawnLocation(); + world = null; + } + /* Set world to loaded */ + public void setWorldLoaded(IWorld w) { + world = w; + this.sealevel = w.getSeaLevel(); // Read actual current sealevel from world + // Update lighting table + float[] lt = w.getDimension().getLightBrightnessTable(); + for (int i = 0; i < 16; i++) { + this.setBrightnessTableEntry(i, lt[i]); + } + } + /* Get light level of block */ + @Override + public int getLightLevel(int x, int y, int z) + { + if(world != null) + return world.getLight(new BlockPos(x, y, z)); + else + return -1; + } + /* Get highest Y coord of given location */ + @Override + public int getHighestBlockYAt(int x, int z) + { + if(world != null) { + return world.getWorld().getChunk(x >> 4, z >> 4).getHeightmap(Type.LIGHT_BLOCKING).getHeight(x & 15, z & 15); + } + else + return -1; + } + /* Test if sky light level is requestable */ + @Override + public boolean canGetSkyLightLevel() + { + return skylight; + } + /* Return sky light level */ + @Override + public int getSkyLightLevel(int x, int y, int z) + { + if(world != null) { + return world.getLightFor(EnumLightType.SKY, new BlockPos(x, y, z)); + } + else + return -1; + } + /** + * Get world environment ID (lower case - normal, the_end, nether) + */ + @Override + public String getEnvironment() + { + return env; + } + /** + * Get map chunk cache for world + */ + @Override + public MapChunkCache getChunkCache(List chunks) + { + if(world != null) { + ForgeMapChunkCache c = new ForgeMapChunkCache(); + c.setChunks(this, chunks); + return c; + } + return null; + } + + public World getWorld() + { + return world.getWorld(); + } + @Override + public Polygon getWorldBorder() { + if (world != null) { + WorldBorder wb = world.getWorldBorder(); + if ((wb != null) && (wb.getDiameter() < 5.9E7)) { + Polygon p = new Polygon(); + p.addVertex(wb.minX(), wb.minZ()); + p.addVertex(wb.minX(), wb.maxZ()); + p.addVertex(wb.maxX(), wb.maxZ()); + p.addVertex(wb.maxX(), wb.minZ()); + return p; + } + } + return null; + } +} diff --git a/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/Proxy.java b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/Proxy.java new file mode 100644 index 00000000..7806a4bd --- /dev/null +++ b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/Proxy.java @@ -0,0 +1,24 @@ +package org.dynmap.forge_1_14_4; + +import net.minecraft.server.MinecraftServer; + +/** + * Server side proxy - methods for creating and cleaning up plugin + */ +public class Proxy +{ + public Proxy() + { + } + public DynmapPlugin startServer(MinecraftServer srv) { + DynmapPlugin plugin = DynmapPlugin.plugin; + if (plugin == null) { + plugin = new DynmapPlugin(srv); + plugin.onEnable(); + } + return plugin; + } + public void stopServer(DynmapPlugin plugin) { + plugin.onDisable(); + } +} diff --git a/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/SnapshotCache.java b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/SnapshotCache.java new file mode 100644 index 00000000..d0ae07e7 --- /dev/null +++ b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/SnapshotCache.java @@ -0,0 +1,191 @@ +package org.dynmap.forge_1_14_4; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.WeakReference; +import java.lang.ref.SoftReference; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.dynmap.utils.DynIntHashMap; + +public class SnapshotCache { + public static class SnapshotRec { + public ChunkSnapshot ss; + public DynIntHashMap tileData; + }; + + private CacheHashMap snapcache; + private ReferenceQueue refqueue; + private long cache_attempts; + private long cache_success; + private boolean softref; + + private static class CacheRec { + Reference ref; + boolean hasbiome; + boolean hasrawbiome; + boolean hasblockdata; + boolean hashighesty; + } + + @SuppressWarnings("serial") + public class CacheHashMap extends LinkedHashMap { + private int limit; + private IdentityHashMap, String> reverselookup; + + public CacheHashMap(int lim) { + super(16, (float)0.75, true); + limit = lim; + reverselookup = new IdentityHashMap, String>(); + } + protected boolean removeEldestEntry(Map.Entry last) { + boolean remove = (size() >= limit); + if(remove && (last != null) && (last.getValue() != null)) { + reverselookup.remove(last.getValue().ref); + } + return remove; + } + } + + /** + * Create snapshot cache + */ + public SnapshotCache(int max_size, boolean softref) { + snapcache = new CacheHashMap(max_size); + refqueue = new ReferenceQueue(); + this.softref = softref; + } + private String getKey(String w, int cx, int cz) { + return w + ":" + cx + ":" + cz; + } + /** + * Invalidate cached snapshot, if in cache + */ + public void invalidateSnapshot(String w, int x, int y, int z) { + String key = getKey(w, x>>4, z>>4); + synchronized(snapcache) { + CacheRec rec = snapcache.remove(key); + if(rec != null) { + snapcache.reverselookup.remove(rec.ref); + rec.ref.clear(); + } + } + //processRefQueue(); + } + /** + * Invalidate cached snapshot, if in cache + */ + public void invalidateSnapshot(String w, int x0, int y0, int z0, int x1, int y1, int z1) { + for(int xx = (x0>>4); xx <= (x1>>4); xx++) { + for(int zz = (z0>>4); zz <= (z1>>4); zz++) { + String key = getKey(w, xx, zz); + synchronized(snapcache) { + CacheRec rec = snapcache.remove(key); + if(rec != null) { + snapcache.reverselookup.remove(rec.ref); + rec.ref.clear(); + } + } + } + } + //processRefQueue(); + } + /** + * Look for chunk snapshot in cache + */ + public SnapshotRec getSnapshot(String w, int chunkx, int chunkz, + boolean blockdata, boolean biome, boolean biomeraw, boolean highesty) { + String key = getKey(w, chunkx, chunkz); + processRefQueue(); + SnapshotRec ss = null; + CacheRec rec; + synchronized(snapcache) { + rec = snapcache.get(key); + if(rec != null) { + ss = rec.ref.get(); + if(ss == null) { + snapcache.reverselookup.remove(rec.ref); + snapcache.remove(key); + } + } + } + if(ss != null) { + if((blockdata && (!rec.hasblockdata)) || + (biome && (!rec.hasbiome)) || + (biomeraw && (!rec.hasrawbiome)) || + (highesty && (!rec.hashighesty))) { + ss = null; + } + } + cache_attempts++; + if(ss != null) cache_success++; + + return ss; + } + /** + * Add chunk snapshot to cache + */ + public void putSnapshot(String w, int chunkx, int chunkz, SnapshotRec ss, + boolean blockdata, boolean biome, boolean biomeraw, boolean highesty) { + String key = getKey(w, chunkx, chunkz); + processRefQueue(); + CacheRec rec = new CacheRec(); + rec.hasblockdata = blockdata; + rec.hasbiome = biome; + rec.hasrawbiome = biomeraw; + rec.hashighesty = highesty; + if (softref) + rec.ref = new SoftReference(ss, refqueue); + else + rec.ref = new WeakReference(ss, refqueue); + synchronized(snapcache) { + CacheRec prevrec = snapcache.put(key, rec); + if(prevrec != null) { + snapcache.reverselookup.remove(prevrec.ref); + } + snapcache.reverselookup.put(rec.ref, key); + } + } + /** + * Process reference queue + */ + private void processRefQueue() { + Reference ref; + while((ref = refqueue.poll()) != null) { + synchronized(snapcache) { + String k = snapcache.reverselookup.remove(ref); + if(k != null) { + snapcache.remove(k); + } + } + } + } + /** + * Get hit rate (percent) + */ + public double getHitRate() { + if(cache_attempts > 0) { + return (100.0*cache_success)/(double)cache_attempts; + } + return 0.0; + } + /** + * Reset cache stats + */ + public void resetStats() { + cache_attempts = cache_success = 0; + } + /** + * Cleanup + */ + public void cleanup() { + if(snapcache != null) { + snapcache.clear(); + snapcache.reverselookup.clear(); + snapcache.reverselookup = null; + snapcache = null; + } + } +} diff --git a/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/VersionCheck.java b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/VersionCheck.java new file mode 100644 index 00000000..472ade1f --- /dev/null +++ b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/VersionCheck.java @@ -0,0 +1,97 @@ +package org.dynmap.forge_1_14_4; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; + +import org.dynmap.DynmapCore; +import org.dynmap.Log; + +public class VersionCheck { + private static final String VERSION_URL = "http://mikeprimm.com/dynmap/releases.php"; + public static void runCheck(final DynmapCore core) { + new Thread(new Runnable() { + public void run() { + doCheck(core); + } + }).start(); + } + + private static int getReleaseVersion(String s) { + int index = s.lastIndexOf('-'); + if(index < 0) + index = s.lastIndexOf('.'); + if(index >= 0) + s = s.substring(0, index); + String[] split = s.split("\\."); + int v = 0; + try { + for(int i = 0; (i < split.length) && (i < 3); i++) { + v += Integer.parseInt(split[i]) << (8 * (2 - i)); + } + } catch (NumberFormatException nfx) {} + return v; + } + + private static int getBuildNumber(String s) { + int index = s.lastIndexOf('-'); + if(index < 0) + index = s.lastIndexOf('.'); + if(index >= 0) + s = s.substring(index+1); + try { + return Integer.parseInt(s); + } catch (NumberFormatException nfx) { + return 99999999; + } + } + + private static void doCheck(DynmapCore core) { + String pluginver = core.getDynmapPluginVersion(); + String platform = core.getDynmapPluginPlatform(); + String platver = core.getDynmapPluginPlatformVersion(); + if((pluginver == null) || (platform == null) || (platver == null)) + return; + HttpURLConnection conn = null; + String loc = VERSION_URL; + int cur_ver = getReleaseVersion(pluginver); + int cur_bn = getBuildNumber(pluginver); + try { + while((loc != null) && (!loc.isEmpty())) { + URL url = new URL(loc); + conn = (HttpURLConnection) url.openConnection(); + conn.setRequestProperty("User-Agent", "Dynmap (" + platform + "/" + platver + "/" + pluginver); + conn.connect(); + loc = conn.getHeaderField("Location"); + } + BufferedReader rdr = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String line = null; + while((line = rdr.readLine()) != null) { + String[] split = line.split(":"); + if(split.length < 4) continue; + /* If our platform and version, or wildcard platform version */ + if(split[0].equals(platform) && (split[1].equals("*") || split[1].equals(platver))) { + int recommended_ver = getReleaseVersion(split[2]); + int recommended_bn = getBuildNumber(split[2]); + if((recommended_ver > cur_ver) || ((recommended_ver == cur_ver) && (recommended_bn > cur_bn))) { /* Newer recommended build */ + Log.info("Version obsolete: new recommended version " + split[2] + " is available."); + } + else if(cur_ver > recommended_ver) { /* Running dev or prerelease? */ + int prerel_ver = getReleaseVersion(split[3]); + int prerel_bn = getBuildNumber(split[3]); + if((prerel_ver > cur_ver) || ((prerel_ver == cur_ver) && (prerel_bn > cur_bn))) { + Log.info("Version obsolete: new prerelease version " + split[3] + " is available."); + } + } + } + } + } catch (Exception x) { + Log.info("Error checking for latest version"); + } finally { + if(conn != null) { + conn.disconnect(); + } + } + } +} diff --git a/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/permissions/FilePermissions.java b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/permissions/FilePermissions.java new file mode 100644 index 00000000..e602c7ff --- /dev/null +++ b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/permissions/FilePermissions.java @@ -0,0 +1,103 @@ +package org.dynmap.forge_1_14_4.permissions; + +import java.io.File; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import net.minecraft.entity.player.EntityPlayer; + +import org.dynmap.ConfigurationNode; +import org.dynmap.Log; +import org.dynmap.forge_1_14_4.DynmapPlugin; + +public class FilePermissions implements PermissionProvider { + private HashMap> perms; + private Set defperms; + + public static FilePermissions create() { + File f = new File("dynmap/permissions.yml"); + if(!f.exists()) + return null; + ConfigurationNode cfg = new ConfigurationNode(f); + cfg.load(); + + Log.info("Using permissions.yml for access control"); + + return new FilePermissions(cfg); + } + + private FilePermissions(ConfigurationNode cfg) { + perms = new HashMap>(); + for(String k : cfg.keySet()) { + List p = cfg.getStrings(k, null); + if(p != null) { + k = k.toLowerCase(); + HashSet pset = new HashSet(); + for(String perm : p) { + pset.add(perm.toLowerCase()); + } + perms.put(k, pset); + if(k.equals("defaultuser")) { + defperms = pset; + } + } + } + } + + private boolean hasPerm(String player, String perm) { + Set ps = perms.get(player); + if((ps != null) && (ps.contains(perm))) { + return true; + } + if(defperms.contains(perm)) { + return true; + } + return false; + } + @Override + public Set hasOfflinePermissions(String player, Set perms) { + player = player.toLowerCase(); + HashSet rslt = new HashSet(); + if(DynmapPlugin.plugin.isOp(player)) { + rslt.addAll(perms); + } + else { + for(String p : perms) { + if(hasPerm(player, p)) { + rslt.add(p); + } + } + } + return rslt; + } + @Override + public boolean hasOfflinePermission(String player, String perm) { + player = player.toLowerCase(); + if(DynmapPlugin.plugin.isOp(player)) { + return true; + } + else { + return hasPerm(player, perm); + } + } + + @Override + public boolean has(EntityPlayer psender, String permission) { + if(psender != null) { + String n = psender.getName().getString().toLowerCase(); + return hasPerm(n, permission); + } + return true; + } + @Override + public boolean hasPermissionNode(EntityPlayer psender, String permission) { + if(psender != null) { + String player = psender.getName().getString().toLowerCase(); + return DynmapPlugin.plugin.isOp(player); + } + return false; + } + +} diff --git a/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/permissions/OpPermissions.java b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/permissions/OpPermissions.java new file mode 100644 index 00000000..db8a0168 --- /dev/null +++ b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/permissions/OpPermissions.java @@ -0,0 +1,51 @@ +package org.dynmap.forge_1_14_4.permissions; + +import java.util.HashSet; +import java.util.Set; + +import net.minecraft.entity.player.EntityPlayer; + +import org.dynmap.Log; +import org.dynmap.forge_1_14_4.DynmapPlugin; + +public class OpPermissions implements PermissionProvider { + public HashSet usrCommands = new HashSet(); + + public OpPermissions(String[] usrCommands) { + for (String usrCommand : usrCommands) { + this.usrCommands.add(usrCommand); + } + Log.info("Using ops.txt for access control"); + } + + @Override + public Set hasOfflinePermissions(String player, Set perms) { + HashSet rslt = new HashSet(); + if(DynmapPlugin.plugin.isOp(player)) { + rslt.addAll(perms); + } + return rslt; + } + @Override + public boolean hasOfflinePermission(String player, String perm) { + return DynmapPlugin.plugin.isOp(player); + } + + @Override + public boolean has(EntityPlayer psender, String permission) { + if(psender != null) { + if(usrCommands.contains(permission)) { + return true; + } + return DynmapPlugin.plugin.isOp(psender.getEntity().getName().getString()); + } + return true; + } + @Override + public boolean hasPermissionNode(EntityPlayer psender, String permission) { + if(psender != null) { + return DynmapPlugin.plugin.isOp(psender.getEntity().getName().getString()); + } + return true; + } +} diff --git a/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/permissions/PermissionProvider.java b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/permissions/PermissionProvider.java new file mode 100644 index 00000000..8473f99c --- /dev/null +++ b/forge-1.14.4/src/main/java/org/dynmap/forge_1_14_4/permissions/PermissionProvider.java @@ -0,0 +1,15 @@ +package org.dynmap.forge_1_14_4.permissions; + +import java.util.Set; + +import net.minecraft.entity.player.EntityPlayer; + +public interface PermissionProvider { + boolean has(EntityPlayer sender, String permission); + boolean hasPermissionNode(EntityPlayer sender, String permission); + + Set hasOfflinePermissions(String player, Set perms); + + boolean hasOfflinePermission(String player, String perm); + +} diff --git a/forge-1.14.4/src/main/resources/META-INF/mods.toml b/forge-1.14.4/src/main/resources/META-INF/mods.toml new file mode 100644 index 00000000..0f19d52b --- /dev/null +++ b/forge-1.14.4/src/main/resources/META-INF/mods.toml @@ -0,0 +1,25 @@ +modLoader="javafml" +loaderVersion="[25,)" +issueTrackerURL="https://github.com/webbukkit/dynmap/issues" +[[mods]] +modId="dynmap" +version="${version}" +displayName="Dynmap" +authors="mikeprimm" +description=''' +Dynamic, Google-maps style rendered maps for your Minecraft server +''' + +[[dependencies.dynmap]] + modId="forge" + mandatory=true + versionRange="[25,)" + ordering="NONE" + # Side this dependency is applied on - BOTH, CLIENT or SERVER + side="SERVER" +[[dependencies.dynmap]] + modId="minecraft" + mandatory=true + versionRange="[1.14.4]" + ordering="NONE" + side="SERVER" diff --git a/forge-1.14.4/src/main/resources/configuration.txt b/forge-1.14.4/src/main/resources/configuration.txt new file mode 100644 index 00000000..4e1ad63f --- /dev/null +++ b/forge-1.14.4/src/main/resources/configuration.txt @@ -0,0 +1,443 @@ +# All paths in this configuration file are relative to Dynmap's data-folder: minecraft_server/dynmap/ + +# All map templates are defined in the templates directory +# To use the HDMap very-low-res (2 ppb) map templates as world defaults, set value to vlowres +# The definitions of these templates are in normal-vlowres.txt, nether-vlowres.txt, and the_end-vlowres.txt +# To use the HDMap low-res (4 ppb) map templates as world defaults, set value to lowres +# The definitions of these templates are in normal-lowres.txt, nether-lowres.txt, and the_end-lowres.txt +# To use the HDMap hi-res (16 ppb) map templates (these can take a VERY long time for initial fullrender), set value to hires +# The definitions of these templates are in normal-hires.txt, nether-hires.txt, and the_end-hires.txt +# To use the HDMap low-res (4 ppb) map templates, with support for boosting resolution selectively to hi-res (16 ppb), set value to low_boost_hi +# The definitions of these templates are in normal-low_boost_hi.txt, nether-low_boost_hi.txt, and the_end-low_boost_hi.txt +# To use the HDMap hi-res (16 ppb) map templates, with support for boosting resolution selectively to vhi-res (32 ppb), set value to hi_boost_vhi +# The definitions of these templates are in normal-hi_boost_vhi.txt, nether-hi_boost_vhi.txt, and the_end-hi_boost_vhi.txt +# To use the HDMap hi-res (16 ppb) map templates, with support for boosting resolution selectively to xhi-res (64 ppb), set value to hi_boost_xhi +# The definitions of these templates are in normal-hi_boost_xhi.txt, nether-hi_boost_xhi.txt, and the_end-hi_boost_xhi.txt +deftemplatesuffix: hires + +# Map storage scheme: only uncommoent one 'type' value +# filetree: classic and default scheme: tree of files, with all map data under the directory indicated by 'tilespath' setting +# sqlite: single SQLite database file (this can get VERY BIG), located at 'dbfile' setting (default is file dynmap.db in data directory) +# mysql: MySQL database, at hostname:port in database, accessed via userid with password +storage: + # Filetree storage (standard tree of image files for maps) + type: filetree + # SQLite db for map storage (uses dbfile as storage location) + #type: sqlite + #dbfile: dynmap.db + # MySQL DB for map storage (at 'hostname':'port' in database 'database' using user 'userid' password 'password' and table prefix 'prefix' + #type: mysql + #hostname: localhost + #port: 3306 + #database: dynmap + #userid: dynmap + #password: dynmap + #prefix: "" + +components: + - class: org.dynmap.ClientConfigurationComponent + + - class: org.dynmap.InternalClientUpdateComponent + sendhealth: true + sendposition: true + allowwebchat: true + webchat-interval: 5 + hidewebchatip: false + trustclientname: false + includehiddenplayers: false + # (optional) if true, color codes in player display names are used + use-name-colors: false + # (optional) if true, player login IDs will be used for web chat when their IPs match + use-player-login-ip: true + # (optional) if use-player-login-ip is true, setting this to true will cause chat messages not matching a known player IP to be ignored + require-player-login-ip: false + # (optional) block player login IDs that are banned from chatting + block-banned-player-chat: true + # Require login for web-to-server chat (requires login-enabled: true) + webchat-requires-login: false + # If set to true, users must have dynmap.webchat permission in order to chat + webchat-permissions: false + # Limit length of single chat messages + chatlengthlimit: 256 + # # Optional - make players hidden when they are inside/underground/in shadows (#=light level: 0=full shadow,15=sky) + # hideifshadow: 4 + # # Optional - make player hidden when they are under cover (#=sky light level,0=underground,15=open to sky) + # hideifundercover: 14 + # # (Optional) if true, players that are crouching/sneaking will be hidden + hideifsneaking: false + # If true, player positions/status is protected (login with ID with dynmap.playermarkers.seeall permission required for info other than self) + protected-player-info: false + # If true, hide players with invisibility potion effects active + hide-if-invisiblity-potion: true + # If true, player names are not shown on map, chat, list + hidenames: false + #- class: org.dynmap.JsonFileClientUpdateComponent + # writeinterval: 1 + # sendhealth: true + # sendposition: true + # allowwebchat: true + # webchat-interval: 5 + # hidewebchatip: false + # includehiddenplayers: false + # use-name-colors: false + # use-player-login-ip: false + # require-player-login-ip: false + # block-banned-player-chat: true + # hideifshadow: 0 + # hideifundercover: 0 + # hideifsneaking: false + # # Require login for web-to-server chat (requires login-enabled: true) + # webchat-requires-login: false + # # If set to true, users must have dynmap.webchat permission in order to chat + # webchat-permissions: false + # # Limit length of single chat messages + # chatlengthlimit: 256 + # hide-if-invisiblity-potion: true + # hidenames: false + + - class: org.dynmap.SimpleWebChatComponent + allowchat: true + # If true, web UI users can supply name for chat using 'playername' URL parameter. 'trustclientname' must also be set true. + allowurlname: false + + # Note: this component is needed for the dmarker commands, and for the Marker API to be available to other plugins + - class: org.dynmap.MarkersComponent + type: markers + showlabel: false + enablesigns: false + # Default marker set for sign markers + default-sign-set: markers + # (optional) add spawn point markers to standard marker layer + showspawn: true + spawnicon: world + spawnlabel: "Spawn" + # (optional) layer for showing offline player's positions (for 'maxofflinetime' minutes after logoff) + showofflineplayers: false + offlinelabel: "Offline" + offlineicon: offlineuser + offlinehidebydefault: true + offlineminzoom: 0 + maxofflinetime: 30 + # (optional) layer for showing player's spawn beds + showspawnbeds: false + spawnbedlabel: "Spawn Beds" + spawnbedicon: bed + spawnbedhidebydefault: true + spawnbedminzoom: 0 + spawnbedformat: "%name%'s bed" + # (optional) Show world border (vanilla 1.8+) + showworldborder: true + worldborderlabel: "Border" + + - class: org.dynmap.ClientComponent + type: chat + allowurlname: false + - class: org.dynmap.ClientComponent + type: chatballoon + focuschatballoons: false + - class: org.dynmap.ClientComponent + type: chatbox + showplayerfaces: true + messagettl: 5 + # Optional: set number of lines in scrollable message history: if set, messagettl is not used to age out messages + #scrollback: 100 + # Optional: set maximum number of lines visible for chatbox + #visiblelines: 10 + # Optional: send push button + sendbutton: false + - class: org.dynmap.ClientComponent + type: playermarkers + showplayerfaces: true + showplayerhealth: true + # If true, show player body too (only valid if showplayerfaces=true + showplayerbody: false + # Option to make player faces small - don't use with showplayerhealth + smallplayerfaces: false + # Optional - make player faces layer hidden by default + hidebydefault: false + # Optional - ordering priority in layer menu (low goes before high - default is 0) + layerprio: 0 + # Optional - label for player marker layer (default is 'Players') + label: "Players" + + #- class: org.dynmap.ClientComponent + # type: digitalclock + - class: org.dynmap.ClientComponent + type: link + + - class: org.dynmap.ClientComponent + type: timeofdayclock + showdigitalclock: true + #showweather: true + # Mouse pointer world coordinate display + - class: org.dynmap.ClientComponent + type: coord + label: "Location" + hidey: false + show-mcr: false + show-chunk: false + + # Note: more than one logo component can be defined + #- class: org.dynmap.ClientComponent + # type: logo + # text: "Dynmap" + # #logourl: "images/block_surface.png" + # linkurl: "http://forums.bukkit.org/threads/dynmap.489/" + # # Valid positions: top-left, top-right, bottom-left, bottom-right + # position: bottom-right + + #- class: org.dynmap.ClientComponent + # type: inactive + # timeout: 1800 # in seconds (1800 seconds = 30 minutes) + # redirecturl: inactive.html + # #showmessage: 'You were inactive for too long.' + + #- class: org.dynmap.TestComponent + # stuff: "This is some configuration-value" + +# Treat hiddenplayers.txt as a whitelist for players to be shown on the map? (Default false) +display-whitelist: false + +# How often a tile gets rendered (in seconds). +renderinterval: 1 + +# How many tiles on update queue before accelerate render interval +renderacceleratethreshold: 60 + +# How often to render tiles when backlog is above renderacceleratethreshold +renderaccelerateinterval: 0.2 + +# How many update tiles to work on at once (if not defined, default is 1/2 the number of cores) +tiles-rendered-at-once: 2 + +# If true, use normal priority threads for rendering (versus low priority) - this can keep rendering +# from starving on busy Windows boxes (Linux JVMs pretty much ignore thread priority), but may result +# in more competition for CPU resources with other processes +usenormalthreadpriority: true + +# Save and restore pending tile renders - prevents their loss on server shutdown or /reload +saverestorepending: true + +# Save period for pending jobs (in seconds): periodic saving for crash recovery of jobs +save-pending-period: 900 + +# Zoom-out tile update period - how often to scan for and process tile updates into zoom-out tiles (in seconds) +zoomoutperiod: 30 + +# Control whether zoom out tiles are validated on startup (can be needed if zoomout processing is interrupted, but can be expensive on large maps) +initial-zoomout-validate: true + +# Default delay on processing of updated tiles, in seconds. This can reduce potentially expensive re-rendering +# of frequently updated tiles (such as due to machines, pistons, quarries or other automation). Values can +# also be set on individual worlds and individual maps. +tileupdatedelay: 30 + +# Tile hashing is used to minimize tile file updates when no changes have occurred - set to false to disable +enabletilehash: true + +# Optional - hide ores: render as normal stone (so that they aren't revealed by maps) +#hideores: true + +# Optional - enabled BetterGrass style rendering of grass and snow block sides +#better-grass: true + +# Optional - enable smooth lighting by default on all maps supporting it (can be set per map as lighting option) +smooth-lighting: true + +# Optional - use world provider lighting table (good for custom worlds with custom lighting curves, like nether) +# false=classic Dynmap lighting curve +use-brightness-table: true + +# Optional - render specific block IDs using the texures and models of another block ID: can be used to hide/disguise specific +# blocks (e.g. make ores look like stone, hide chests) or to provide simple support for rendering unsupported custom blocks +block-id-alias: +# "14": 1 +# "15": 1 +# "16": 1 + +# Default image format for HDMaps (png, jpg, jpg-q75, jpg-q80, jpg-q85, jpg-q90, jpg-q95, jpg-q100) +# Has no effect on maps with explicit format settings +image-format: png + +# use-generated-textures: if true, use generated textures (same as client); false is static water/lava textures +# correct-water-lighting: if true, use corrected water lighting (same as client); false is legacy water (darker) +# transparent-leaves: if true, leaves are transparent (lighting-wise): false is needed for some Spout versions that break lighting on leaf blocks +use-generated-textures: true +correct-water-lighting: true +transparent-leaves: true + +# ctm-support: if true, Connected Texture Mod (CTM) in texture packs is enabled (default) +ctm-support: true +# custom-colors-support: if true, Custom Colors in texture packs is enabled (default) +custom-colors-support: true + +# Control loading of player faces (if set to false, skins are never fetched) +#fetchskins: false + +# Control updating of player faces, once loaded (if faces are being managed by other apps or manually) +#refreshskins: false + +# Customize URL used for fetching player skins (%player% is macro for name) +skin-url: "http://skins.minecraft.net/MinecraftSkins/%player%.png" + +# Control behavior for new (1.0+) compass orientation (sunrise moved 90 degrees: east is now what used to be south) +# default is 'newrose' (preserve pre-1.0 maps, rotate rose) +# 'newnorth' is used to rotate maps and rose (requires fullrender of any HDMap map - same as 'newrose' for FlatMap or KzedMap) +compass-mode: newnorth + +# Triggers for automatic updates : blockupdate-with-id is debug for breaking down updates by ID:meta +# To disable, set just 'none' and comment/delete the rest +render-triggers: + - blockupdate + #- blockupdate-with-id + #- lightingupdate + - chunkpopulate + - chunkgenerate + #- none + +# Title for the web page - if not specified, defaults to the server's name (unless it is the default of 'Unknown Server') +#webpage-title: "My Awesome Server Map" + +# The path where the tile-files are placed. +tilespath: web/tiles + +# The path where the web-files are located. +webpath: web + +# The path were the /dynmapexp command exports OBJ ZIP files +exportpath: export + +# The network-interface the webserver will bind to (0.0.0.0 for all interfaces, 127.0.0.1 for only local access). +# If not set, uses same setting as server in server.properties (or 0.0.0.0 if not specified) +#webserver-bindaddress: 0.0.0.0 + +# The TCP-port the webserver will listen on. +webserver-port: 8123 + +# Maximum concurrent session on internal web server - limits resources used in Bukkit server +max-sessions: 30 + +# Disables Webserver portion of Dynmap (Advanced users only) +disable-webserver: false + +# Enable/disable having the web server allow symbolic links (true=compatible with existing code, false=more secure (default)) +allow-symlinks: true + +# Enable login support +login-enabled: false +# Require login to access website (requires login-enabled: true) +login-required: false + +# Period between tile renders for fullrender, in seconds (non-zero to pace fullrenders, lessen CPU load) +timesliceinterval: 0.0 + +# Maximum chunk loads per server tick (1/20th of a second) - reducing this below 90 will impact render performance, but also will reduce server thread load +maxchunkspertick: 200 + +# Progress report interval for fullrender/radiusrender, in tiles. Must be 100 or greater +progressloginterval: 100 + +# Parallel fullrender: if defined, number of concurrent threads used for fullrender or radiusrender +# Note: setting this will result in much more intensive CPU use, some additional memory use. Caution should be used when +# setting this to equal or exceed the number of physical cores on the system. +#parallelrendercnt: 4 + +# Interval the browser should poll for updates. +updaterate: 2000 + +# If nonzero, server will pause fullrender/radiusrender processing when 'fullrenderplayerlimit' or more users are logged in +fullrenderplayerlimit: 0 +# If nonzero, server will pause update render processing when 'updateplayerlimit' or more users are logged in +updateplayerlimit: 0 +# Target limit on server thread use - msec per tick +per-tick-time-limit: 50 +# If TPS of server is below this setting, update renders processing is paused +update-min-tps: 18.0 +# If TPS of server is below this setting, full/radius renders processing is paused +fullrender-min-tps: 18.0 +# If TPS of server is below this setting, zoom out processing is paused +zoomout-min-tps: 18.0 + +showplayerfacesinmenu: true + +# Control whether players that are hidden or not on current map are grayed out (true=yes) +grayplayerswhenhidden: true + +# Set sidebaropened: 'true' to pin menu sidebar opened permanently, 'pinned' to default the sidebar to pinned, but allow it to unpin +#sidebaropened: true + +# Customized HTTP response headers - add 'id: value' pairs to all HTTP response headers (internal web server only) +#http-response-headers: +# Access-Control-Allow-Origin: "my-domain.com" +# X-Custom-Header-Of-Mine: "MyHeaderValue" + +# Trusted proxies for web server - which proxy addresses are trusted to supply valid X-Forwarded-For fields +trusted-proxies: + - "127.0.0.1" + - "0:0:0:0:0:0:0:1" + +joinmessage: "%playername% joined" +quitmessage: "%playername% quit" +spammessage: "You may only chat once every %interval% seconds." +# format for messages from web: %playername% substitutes sender ID (typically IP), %message% includes text +webmsgformat: "&color;2[WEB] %playername%: &color;f%message%" + +# Control whether layer control is presented on the UI (default is true) +showlayercontrol: true + +# Enable checking for banned IPs via banned-ips.txt (internal web server only) +check-banned-ips: true + +# Default selection when map page is loaded +defaultzoom: 0 +defaultworld: world +defaultmap: flat +# (optional) Zoom level and map to switch to when following a player, if possible +#followzoom: 3 +#followmap: surface + +# If true, make persistent record of IP addresses used by player logins, to support web IP to player matching +persist-ids-by-ip: true + +# If true, map text to cyrillic +cyrillic-support: false + +# Messages to customize +msg: + maptypes: "Map Types" + players: "Players" + chatrequireslogin: "Chat Requires Login" + chatnotallowed: "You are not permitted to send chat messages" + hiddennamejoin: "Player joined" + hiddennamequit: "Player quit" + +# URL for client configuration (only need to be tailored for proxies or other non-standard configurations) +url: + # configuration URL + #configuration: "up/configuration" + # update URL + #update: "up/world/{world}/{timestamp}" + # sendmessage URL + #sendmessage: "up/sendmessage" + # login URL + #login: "up/login" + # register URL + #register: "up/register" + # tiles base URL + #tiles: "tiles/" + # markers base URL + #markers: "tiles/" + # Snapshot cache size, in chunks +snapshotcachesize: 500 +# Snapshot cache uses soft references (true), else weak references (false) +soft-ref-cache: true + +# Set to true to enable verbose startup messages - can help with debugging map configuration problems +# Set to false for a much quieter startup log +verbose: false + +# Enables debugging. +#debuggers: +# - class: org.dynmap.debug.LogDebugger +# Debug: dump blocks missing render data +dump-missing-blocks: false diff --git a/forge-1.14.4/src/main/resources/dynmap_at.cfg b/forge-1.14.4/src/main/resources/dynmap_at.cfg new file mode 100644 index 00000000..e97eb6d8 --- /dev/null +++ b/forge-1.14.4/src/main/resources/dynmap_at.cfg @@ -0,0 +1,4 @@ +public net.minecraft.world.gen.ChunkProviderServer field_73247_e # chunkLoader +public net.minecraft.world.chunk.storage.AnvilChunkLoader field_75825_d # chunkSaveLocation +public net.minecraft.world.biome.Biome field_76791_y # biomeName +public net.minecraft.world.chunk.Chunk field_76641_n # lastSaveTime diff --git a/forge-1.14.4/src/main/resources/pack.mcmeta b/forge-1.14.4/src/main/resources/pack.mcmeta new file mode 100644 index 00000000..ee818eac --- /dev/null +++ b/forge-1.14.4/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "description": "Dynmap resources", + "pack_format": 4 + } +} diff --git a/forge-1.14.4/src/main/resources/permissions.yml.example b/forge-1.14.4/src/main/resources/permissions.yml.example new file mode 100644 index 00000000..a25f9adc --- /dev/null +++ b/forge-1.14.4/src/main/resources/permissions.yml.example @@ -0,0 +1,27 @@ +# +# Sample permissions.yml for dynmap - trivial, flat-file based permissions for dynmap features +# To use, copy this file to dynmap/permissions.yml, and edit appropriate. File is YAML format. +# +# All operators have full permissions to all functions. +# All users receive the permissions under the 'defaultuser' section +# Specific users can be given more permissions by defining a section with their name containing their permisssions +# All permissions correspond to those documented here (https://github.com/webbukkit/dynmap/wiki/Permissions), but +# do NOT have the 'dynmap.' prefix when used here (e.g. 'dynmap.fullrender' permission is just 'fullrender' here). +# +defaultuser: + - render + - show.self + - hide.self + - sendtoweb + - stats + - marker.list + - marker.listsets + - marker.icons + - webregister + - webchat + #- marker.sign + +#playername1: +# - fullrender +# - cancelrender +# - radiusrender diff --git a/gradle.properties b/gradle.properties index 4e52490b..857a852f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,3 @@ org.gradle.jvmargs=-Xmx2G org.gradle.parallel=true +org.gradle.daemon=false \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 0d4a9516871afd710a9d84d89e31ba77745607bd..7a3265ee94c0ab25cf079ac8ccdf87f41d455d42 100644 GIT binary patch delta 23104 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 HKcoKx#<@Jp diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a95009c3..949819d2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip diff --git a/settings.gradle b/settings.gradle index e4124566..3940b6eb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,6 +10,7 @@ include ':bukkit-helper' include ':dynmap-api' include ':DynmapCore' include ':DynmapCoreAPI' +//include ':forge-1.14.4' include ':forge-1.13.2' include ':forge-1.12.2' include ':forge-1.11.2' @@ -28,6 +29,7 @@ project(':bukkit-helper').projectDir = "$rootDir/bukkit-helper" as File project(':dynmap-api').projectDir = "$rootDir/dynmap-api" as File project(':DynmapCore').projectDir = "$rootDir/DynmapCore" as File project(':DynmapCoreAPI').projectDir = "$rootDir/DynmapCoreAPI" as File +//project(':forge-1.14.4').projectDir = "$rootDir/forge-1.14.4" as File project(':forge-1.13.2').projectDir = "$rootDir/forge-1.13.2" as File project(':forge-1.12.2').projectDir = "$rootDir/forge-1.12.2" as File project(':forge-1.11.2').projectDir = "$rootDir/forge-1.11.2" as File diff --git a/spigot/src/main/java/org/dynmap/bukkit/Helper.java b/spigot/src/main/java/org/dynmap/bukkit/Helper.java index 70711d53..712233b8 100644 --- a/spigot/src/main/java/org/dynmap/bukkit/Helper.java +++ b/spigot/src/main/java/org/dynmap/bukkit/Helper.java @@ -5,10 +5,7 @@ import org.dynmap.Log; import org.dynmap.bukkit.helper.BukkitVersionHelper; import org.dynmap.bukkit.helper.BukkitVersionHelperCB; import org.dynmap.bukkit.helper.BukkitVersionHelperGlowstone; -import org.dynmap.bukkit.helper.v113.BukkitVersionHelperSpigot113; -import org.dynmap.bukkit.helper.v113_1.BukkitVersionHelperSpigot113_1; import org.dynmap.bukkit.helper.v113_2.BukkitVersionHelperSpigot113_2; -import org.dynmap.bukkit.helper.v114.BukkitVersionHelperSpigot114; import org.dynmap.bukkit.helper.v114_1.BukkitVersionHelperSpigot114_1; import org.dynmap.bukkit.helper.v115.BukkitVersionHelperSpigot115; @@ -39,22 +36,13 @@ public class Helper { else if (v.contains("(MC: 1.15)") || v.contains("(MC: 1.15.")) { BukkitVersionHelper.helper = new BukkitVersionHelperSpigot115(); } - else if (v.contains("(MC: 1.14.1)") || v.contains("(MC: 1.14.2)") || + else if (v.contains("(MC: 1.14)") || v.contains("(MC: 1.14.1)") || v.contains("(MC: 1.14.2)") || v.contains("(MC: 1.14.3)") || v.contains("(MC: 1.14.4)")) { BukkitVersionHelper.helper = new BukkitVersionHelperSpigot114_1(); } - else if (v.contains("(MC: 1.14)")) { - BukkitVersionHelper.helper = new BukkitVersionHelperSpigot114(); - } else if (v.contains("(MC: 1.13.2)")) { BukkitVersionHelper.helper = new BukkitVersionHelperSpigot113_2(); } - else if (v.contains("(MC: 1.13.1)")) { - BukkitVersionHelper.helper = new BukkitVersionHelperSpigot113_1(); - } - else if (v.contains("(MC: 1.13)")) { - BukkitVersionHelper.helper = new BukkitVersionHelperSpigot113(); - } else { BukkitVersionHelper.helper = new BukkitVersionHelperCB(); }