diff --git a/README.md b/README.md index 4397ac0a..00c532bc 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ The following target platforms are supported, and you can find them at the links | Fabric | 1.16.4 | `Dynmap--fabric-1.16.4.jar` | [Curseforge](https://www.curseforge.com/minecraft/mc-mods/dynmapforge) | | Fabric | 1.17 | `Dynmap--fabric-1.17.jar` | [Curseforge](https://www.curseforge.com/minecraft/mc-mods/dynmapforge) | | Fabric | 1.17.1 | `Dynmap--fabric-1.17.1.jar` | [Curseforge](https://www.curseforge.com/minecraft/mc-mods/dynmapforge) | +| Fabric | 1.18 | `Dynmap--fabric-1.18.jar` | [Curseforge](https://www.curseforge.com/minecraft/mc-mods/dynmapforge) | # Data Storage Dynmap supports the following storage backends: diff --git a/fabric-1.18/.gitignore b/fabric-1.18/.gitignore new file mode 100644 index 00000000..8b87af68 --- /dev/null +++ b/fabric-1.18/.gitignore @@ -0,0 +1,32 @@ +# gradle + +.gradle/ +build/ +out/ +classes/ + +# eclipse + +*.launch + +# idea + +.idea/ +*.iml +*.ipr +*.iws + +# vscode + +.settings/ +.vscode/ +bin/ +.classpath +.project + +# fabric + +run/ + +# other +*.log diff --git a/fabric-1.18/build.gradle b/fabric-1.18/build.gradle new file mode 100644 index 00000000..824a9af0 --- /dev/null +++ b/fabric-1.18/build.gradle @@ -0,0 +1,85 @@ +buildscript { + repositories { + maven { url = 'https://maven.fabricmc.net/' } + jcenter() + mavenCentral() + } + dependencies { + classpath group: 'net.fabricmc', name: 'fabric-loom', version: '0.10-SNAPSHOT' + } +} +apply plugin: 'fabric-loom' +loom { + runConfigs.configureEach { + ideConfigGenerated = true + } +} + +sourceCompatibility = JavaVersion.VERSION_17 +targetCompatibility = JavaVersion.VERSION_17 + +archivesBaseName = project.archives_base_name +version = parent.version +group = parent.group + +configurations { + shadow + compile.extendsFrom(shadow) +} + +dependencies { + //to change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + // Fabric API. This is technically optional, but you probably want it anyway. + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" + + compileOnly group: 'com.google.code.findbugs', name: 'jsr305', version: '3.0.2' + + shadow project(path: ':DynmapCore', configuration: 'shadow') + shadow project(path: ':DynmapCoreAPI', configuration: 'shadow') + + // PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs. + // You may need to force-disable transitiveness on them. +} + +processResources { + filesMatching('fabric.mod.json') { + expand "version": project.version + } +} + +// ensure that the encoding is set to UTF-8, no matter what the system default is +// this fixes some edge cases with special characters not displaying correctly +// see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html +tasks.withType(JavaCompile) { + options.encoding = "UTF-8" +} + +// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task +// if it is present. +// If you remove this task, sources will not be generated. +task sourcesJar(type: Jar, dependsOn: classes) { + classifier = "sources" + from sourceSets.main.allSource +} + +jar { + duplicatesStrategy = 'include' + from "LICENSE" + from { + configurations.shadow.collect { it.toString().contains("guava") ? null : it.isDirectory() ? it : zipTree(it) } + } +} + +remapJar { + archiveName = "${archivesBaseName}-${version}-fabric-${project.minecraft_version}.jar" + destinationDir = file '../target' +} + +remapJar.doLast { + task -> + ant.checksum file: task.archivePath +} diff --git a/fabric-1.18/gradle.properties b/fabric-1.18/gradle.properties new file mode 100644 index 00000000..d64c6a50 --- /dev/null +++ b/fabric-1.18/gradle.properties @@ -0,0 +1,14 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx1G + +# Mod Properties +archives_base_name=Dynmap + +# Fabric Properties +# check these on https://fabricmc.net/versions.html +minecraft_version=1.18 +yarn_mappings=1.18+build.1 +loader_version=0.12.6 + +#Fabric api +fabric_version=0.43.1+1.18 diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/ChunkSnapshot.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/ChunkSnapshot.java new file mode 100644 index 00000000..688e7904 --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/ChunkSnapshot.java @@ -0,0 +1,330 @@ +package org.dynmap.fabric_1_18; + +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtList; +import net.minecraft.util.collection.PackedIntegerArray; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.WordPackedArray; +import org.dynmap.Log; +import org.dynmap.renderer.DynmapBlockState; + +import java.util.Arrays; +import java.util.LinkedList; + +/** + * 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 sectionOffset; + 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+1]; + this.sectionOffset = 0; + /* 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 static class StateListException extends Exception { + private static final long serialVersionUID = 1L; + private static boolean loggedOnce = false; + + public StateListException(int x, int z, int actualLength, int expectedLength, int expectedLegacyLength) { + if (Log.verbose || !loggedOnce) { + loggedOnce = true; + Log.info("Skipping chunk at x=" + x + ",z=" + z + ". Expected statelist of length " + expectedLength + " or " + expectedLegacyLength + " but got " + actualLength + ". This can happen if the chunk was not yet converted to the 1.16 format which can be fixed by visiting the chunk."); + if (!Log.verbose) { + Log.info("You will only see this message once. Turn on verbose logging in the configuration to see all messages."); + } + } + } + } + + public ChunkSnapshot(NbtCompound nbt, int worldheight) throws StateListException { + 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 */ + LinkedList
sections = new LinkedList
(); + int sectoff = 0; // Default to zero + int sectcnt = 0; + /* Fill with empty data */ + for (int i = 0; i <= this.sectionCnt; i++) { + sections.add(empty_section); + sectcnt++; + } + /* Get sections */ + NbtList sect = nbt.getList("sections", 10); + for (int i = 0; i < sect.size(); i++) { + NbtCompound sec = sect.getCompound(i); + int secnum = sec.getByte("Y"); + // Beyond end - extend up + while (secnum >= (sectcnt - sectoff)) { + sections.addLast(empty_section); // Pad with empty + sectcnt++; + } + // Negative - see if we need to extend sectionOffset + while ((secnum + sectoff) < 0) { + sections.addFirst(empty_section); // Pad with empty + sectoff++; + sectcnt++; + } + //System.out.println("section(" + secnum + ")=" + sec.asString()); + // Create normal section to initialize + StdSection cursect = new StdSection(); + sections.set(secnum + sectoff, cursect); + DynmapBlockState[] states = cursect.states; + DynmapBlockState[] palette = null; + NbtCompound block_states = sec.getCompound("block_states"); + // If we've block state data, process non-empty section + if (block_states.contains("data", 12)) { + long[] statelist = block_states.getLongArray("data"); + NbtList plist = block_states.getList("palette", 10); + palette = new DynmapBlockState[plist.size()]; + for (int pi = 0; pi < plist.size(); pi++) { + NbtCompound tc = plist.getCompound(pi); + String pname = tc.getString("Name"); + if (tc.contains("Properties")) { + StringBuilder statestr = new StringBuilder(); + NbtCompound prop = tc.getCompound("Properties"); + for (String pid : prop.getKeys()) { + if (statestr.length() > 0) statestr.append(','); + statestr.append(pid).append('=').append(prop.get(pid).asString()); + } + palette[pi] = DynmapBlockState.getStateByNameAndState(pname, statestr.toString()); + } + if (palette[pi] == null) { + palette[pi] = DynmapBlockState.getBaseStateByName(pname); + } + if (palette[pi] == null) { + palette[pi] = DynmapBlockState.AIR; + } + } + + PackedIntegerArray db = null; + WordPackedArray dbp = null; + + int bitsperblock = (statelist.length * 64) / 4096; + int expectedStatelistLength = (4096 + (64 / bitsperblock) - 1) / (64 / bitsperblock); + if (statelist.length == expectedStatelistLength) { + db = new PackedIntegerArray(bitsperblock, 4096, statelist); + } else { + int expectedLegacyStatelistLength = MathHelper.roundUpToMultiple(bitsperblock * 4096, 64) / 64; + if (statelist.length == expectedLegacyStatelistLength) { + dbp = new WordPackedArray(bitsperblock, 4096, statelist); + } else { + throw new StateListException(x, z, statelist.length, expectedStatelistLength, expectedLegacyStatelistLength); + } + } + + if (bitsperblock > 8) { // Not palette + for (int j = 0; j < 4096; j++) { + int v = db != null ? db.get(j) : dbp.get(j); + states[j] = DynmapBlockState.getStateByGlobalIndex(v); + } + } else { + for (int j = 0; j < 4096; j++) { + int v = db != null ? db.get(j) : dbp.get(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 */ + // TODO: Update to new 1.18 format (3D biomes, sectioned): + this.biome = new int[COLUMNS_PER_CHUNK]; + if (nbt.contains("Biomes")) { + int[] bb = nbt.getIntArray("Biomes"); + if (bb != null) { + // If v1.15+ format + if (bb.length > COLUMNS_PER_CHUNK) { + // For now, just pad the grid with the first 16 + for (int i = 0; i < COLUMNS_PER_CHUNK; i++) { + int off = ((i >> 4) & 0xC) + ((i >> 2) & 0x3); + int bv = bb[off + 64]; // Offset to y=64 + if (bv < 0) bv = 0; + this.biome[i] = bv; + } + } else { // Else, older chunks + for (int i = 0; i < bb.length; i++) { + int bv = bb[i]; + if (bv < 0) bv = 0; + this.biome[i] = bv; + } + } + } + } + // Finalize sections array + this.section = sections.toArray(new Section[sections.size()]); + this.sectionOffset = sectoff; + } + + public int getX() { + return x; + } + + public int getZ() { + return z; + } + + public DynmapBlockState getBlockType(int x, int y, int z) + { + int idx = (y >> 4) + sectionOffset; + if ((idx < 0) || (idx >= section.length)) return DynmapBlockState.AIR; + return section[idx].getBlockType(x, y, z); + } + + public int getBlockSkyLight(int x, int y, int z) + { + int idx = (y >> 4) + sectionOffset; + if ((idx < 0) || (idx >= section.length)) return 15; + return section[idx].getBlockSkyLight(x, y, z); + } + + public int getBlockEmittedLight(int x, int y, int z) + { + int idx = (y >> 4) + sectionOffset; + if ((idx < 0) || (idx >= section.length)) return 0; + return section[idx].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) + { + int idx = sy + sectionOffset; + if ((idx < 0) || (idx >= section.length)) return true; + return section[idx].isEmpty(); + } + + public long getInhabitedTicks() { + return inhabitedTicks; + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/DynmapMod.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/DynmapMod.java new file mode 100644 index 00000000..274fdc3a --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/DynmapMod.java @@ -0,0 +1,50 @@ +package org.dynmap.fabric_1_18; + +import net.fabricmc.api.ModInitializer; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import org.dynmap.DynmapCore; +import org.dynmap.Log; + +import java.io.File; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class DynmapMod implements ModInitializer { + private static final String MODID = "dynmap"; + private static final ModContainer MOD_CONTAINER = FabricLoader.getInstance().getModContainer(MODID) + .orElseThrow(() -> new RuntimeException("Failed to get mod container: " + MODID)); + // The instance of your mod that Fabric uses. + public static DynmapMod instance; + + public static DynmapPlugin plugin; + public static File jarfile; + public static String ver; + public static boolean useforcedchunks; + + @Override + public void onInitialize() { + instance = this; + + Path path = MOD_CONTAINER.getRootPath(); + try { + jarfile = new File(DynmapCore.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + } catch (URISyntaxException e) { + Log.severe("Unable to get DynmapCore jar path", e); + } + + if (path.getFileSystem().provider().getScheme().equals("jar")) { + path = Paths.get(path.getFileSystem().toString()); + jarfile = path.toFile(); + } + + ver = MOD_CONTAINER.getMetadata().getVersion().getFriendlyString(); + + Log.setLogger(new FabricLogger()); + org.dynmap.modsupport.ModSupportImpl.init(); + + // Initialize the plugin, we will enable it fully when the server starts. + plugin = new DynmapPlugin(); + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/DynmapPlugin.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/DynmapPlugin.java new file mode 100644 index 00000000..9cdc46bf --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/DynmapPlugin.java @@ -0,0 +1,885 @@ +package org.dynmap.fabric_1_18; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerChunkEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerWorldEvents; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.block.FluidBlock; +import net.minecraft.block.Material; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.Item; +import net.minecraft.network.ClientConnection; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ChunkHolder; +import net.minecraft.server.world.ServerChunkManager; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.util.Identifier; +import net.minecraft.util.collection.IdList; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.util.registry.Registry; +import net.minecraft.world.World; +import net.minecraft.world.WorldAccess; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.ChunkSection; +import net.minecraft.world.chunk.ChunkStatus; +import net.minecraft.world.chunk.WorldChunk; +import org.dynmap.*; +import org.dynmap.common.BiomeMap; +import org.dynmap.common.DynmapCommandSender; +import org.dynmap.common.DynmapListenerManager; +import org.dynmap.common.DynmapPlayer; +import org.dynmap.fabric_1_18.command.DmapCommand; +import org.dynmap.fabric_1_18.command.DmarkerCommand; +import org.dynmap.fabric_1_18.command.DynmapCommand; +import org.dynmap.fabric_1_18.command.DynmapExpCommand; +import org.dynmap.fabric_1_18.event.BlockEvents; +import org.dynmap.fabric_1_18.event.ChunkDataEvents; +import org.dynmap.fabric_1_18.event.CustomServerLifecycleEvents; +import org.dynmap.fabric_1_18.event.PlayerEvents; +import org.dynmap.fabric_1_18.mixin.BiomeEffectsAccessor; +import org.dynmap.fabric_1_18.mixin.ThreadedAnvilChunkStorageAccessor; +import org.dynmap.fabric_1_18.permissions.FilePermissions; +import org.dynmap.fabric_1_18.permissions.OpPermissions; +import org.dynmap.fabric_1_18.permissions.PermissionProvider; +import org.dynmap.permissions.PermissionsHandler; +import org.dynmap.renderer.DynmapBlockState; + +import java.io.File; +import java.util.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.regex.Pattern; + +public class DynmapPlugin { + // FIXME: Fix package-private fields after splitting is done + DynmapCore core; + private PermissionProvider permissions; + private boolean core_enabled; + public SnapshotCache sscache; + public PlayerList playerList; + MapManager mapManager; + /** + * Server is set when running and unset at shutdown. + */ + private net.minecraft.server.MinecraftServer server; + public static DynmapPlugin plugin; + ChatHandler chathandler; + private HashMap sortWeights = new HashMap(); + private HashMap worlds = new HashMap(); + private WorldAccess last_world; + private FabricWorld last_fworld; + private Map players = new HashMap(); + private FabricServer fserver; + private boolean tickregistered = false; + // TPS calculator + double tps; + long lasttick; + long avgticklen; + // Per tick limit, in nsec + long perTickLimit = (50000000); // 50 ms + private boolean useSaveFolder = true; + + private static final String[] TRIGGER_DEFAULTS = {"blockupdate", "chunkpopulate", "chunkgenerate"}; + + static final Pattern patternControlCode = Pattern.compile("(?i)\\u00A7[0-9A-FK-OR]"); + + DynmapPlugin() { + plugin = this; + // Fabric events persist between server instances + ServerLifecycleEvents.SERVER_STARTING.register(this::serverStart); + CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> registerCommands(dispatcher)); + CustomServerLifecycleEvents.SERVER_STARTED_PRE_WORLD_LOAD.register(this::serverStarted); + ServerLifecycleEvents.SERVER_STOPPING.register(this::serverStop); + } + + int getSortWeight(String name) { + return sortWeights.getOrDefault(name, 0); + } + + void setSortWeight(String name, int wt) { + sortWeights.put(name, wt); + } + + void dropSortWeight(String name) { + sortWeights.remove(name); + } + + public static class BlockUpdateRec { + WorldAccess w; + String wid; + int x, y, z; + } + + ConcurrentLinkedQueue blockupdatequeue = new ConcurrentLinkedQueue(); + + public static DynmapBlockState[] stateByID; + + private Map knownloadedchunks = new HashMap(); + + private void addKnownChunk(FabricWorld fw, ChunkPos pos) { + LongOpenHashSet cset = knownloadedchunks.get(fw.getName()); + if (cset == null) { + cset = new LongOpenHashSet(); + knownloadedchunks.put(fw.getName(), cset); + } + cset.add(pos.toLong()); + } + + private void removeKnownChunk(FabricWorld fw, ChunkPos pos) { + LongOpenHashSet cset = knownloadedchunks.get(fw.getName()); + if (cset != null) { + cset.remove(pos.toLong()); + } + } + + private boolean checkIfKnownChunk(FabricWorld fw, ChunkPos pos) { + LongOpenHashSet cset = knownloadedchunks.get(fw.getName()); + if (cset != null) { + return cset.contains(pos.toLong()); + } + return false; + } + + /** + * 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 + + IdList bsids = Block.STATE_IDS; + + DynmapBlockState basebs = null; + Block baseb = null; + int baseidx = 0; + + Iterator iter = bsids.iterator(); + while (iter.hasNext()) { + BlockState bs = iter.next(); + int idx = bsids.getRawId(bs); + if (idx >= stateByID.length) { + int plen = stateByID.length; + stateByID = Arrays.copyOf(stateByID, idx*11/10); // grow array by 10% + 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; + } + + Identifier ui = Registry.BLOCK.getId(b); + 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 (net.minecraft.state.property.Property 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(); + } + if ((!bs.getFluidState().isEmpty()) && !(bs.getBlock() instanceof FluidBlock)) { + dbs.setWaterlogged(); + } + } + } +// 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.byRawId(id); + } + + public static final ClientConnection getNetworkManager(ServerPlayNetworkHandler nh) { + return nh.connection; + } + + FabricPlayer getOrAddPlayer(ServerPlayerEntity player) { + String name = player.getName().getString(); + FabricPlayer fp = players.get(name); + if (fp != null) { + fp.player = player; + } else { + fp = new FabricPlayer(this, player); + players.put(name, fp); + } + return fp; + } + + static class ChatMessage { + String message; + ServerPlayerEntity sender; + } + + ConcurrentLinkedQueue msgqueue = new ConcurrentLinkedQueue(); + + public static class ChatHandler { + private final DynmapPlugin plugin; + + ChatHandler(DynmapPlugin plugin) { + this.plugin = plugin; + } + + public void handleChat(ServerPlayerEntity player, String message) { + if (!message.startsWith("/")) { + ChatMessage cm = new ChatMessage(); + cm.message = message; + cm.sender = player; + plugin.msgqueue.add(cm); + } + } + } + + public FabricServer getFabricServer() { + return fserver; + } + + private void serverStart(MinecraftServer server) { + // Set the server so we don't NPE during setup + this.server = server; + this.fserver = new FabricServer(this, server); + this.onEnable(); + } + + private void serverStarted(MinecraftServer server) { + this.onStart(); + if (core != null) { + core.serverStarted(); + } + } + + private void serverStop(MinecraftServer server) { + this.onDisable(); + this.server = null; + } + + public boolean isOp(String player) { + String[] ops = server.getPlayerManager().getOpList().getNames(); + + for (String op : ops) { + if (op.equalsIgnoreCase(player)) { + return true; + } + } + + // TODO: Consider whether cheats are enabled for integrated server + return server.isSingleplayer() && player.equalsIgnoreCase(server.getSinglePlayerName()); + } + + boolean hasPerm(PlayerEntity psender, String permission) { + PermissionsHandler ph = PermissionsHandler.getHandler(); + if ((psender != null) && ph.hasPermission(psender.getName().getString(), permission)) { + return true; + } + return permissions.has(psender, permission); + } + + boolean hasPermNode(PlayerEntity psender, String permission) { + PermissionsHandler ph = PermissionsHandler.getHandler(); + if ((psender != null) && ph.hasPermissionNode(psender.getName().getString(), permission)) { + return true; + } + return permissions.hasPermissionNode(psender, permission); + } + + 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; + } + + 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); + } + + void setChatHandler(ChatHandler chatHandler) { + plugin.chathandler = chatHandler; + } + + public class TexturesPayload { + public long timestamp; + public String profileId; + public String profileName; + public boolean isPublic; + public Map textures; + + } + + public class ProfileTexture { + public String url; + } + + public void loadExtraBiomes(String mcver) { + int cnt = 0; + BiomeMap.loadWellKnownByVersion(mcver); + + Registry biomeRegistry = getFabricServer().getBiomeRegistry(); + Biome[] list = getFabricServer().getBiomeList(biomeRegistry); + + for (int i = 0; i < list.length; i++) { + Biome bb = list[i]; + if (bb != null) { + String id = biomeRegistry.getId(bb).getPath(); + float tmp = bb.getTemperature(), hum = bb.getDownfall(); + int watermult = ((BiomeEffectsAccessor) bb.getEffects()).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() { + Registry biomeRegistry = getFabricServer().getBiomeRegistry(); + Biome[] list = getFabricServer().getBiomeList(biomeRegistry); + String[] lst = new String[list.length]; + for (int i = 0; i < list.length; i++) { + Biome bb = list[i]; + if (bb != null) { + lst[i] = biomeRegistry.getId(bb).getPath(); + } + } + return lst; + } + + public void onEnable() { + /* Get MC version */ + String mcver = server.getVersion(); + + /* 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()) { + 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); + FabricMapChunkCache.init(); + core.setTriggerDefault(TRIGGER_DEFAULTS); + core.setBiomeNames(getBiomeNames()); + + if (!core.initConfiguration(null)) { + return; + } + // Extract default permission example, if needed + File filepermexample = new File(core.getDataFolder(), "permissions.yml.example"); + core.createDefaultFileFromResource("/permissions.yml.example", filepermexample); + + DynmapCommonAPIListener.apiInitialized(core); + } + + private DynmapCommand dynmapCmd; + private DmapCommand dmapCmd; + private DmarkerCommand dmarkerCmd; + private DynmapExpCommand dynmapexpCmd; + + public void registerCommands(CommandDispatcher cd) { + 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) { + ServerTickEvents.END_SERVER_TICK.register(server -> fserver.tickEvent(server)); + 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(); + + for (FabricWorld 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(DynmapListenerManager.EventType.WORLD_LOAD, w); + } + } + } + core.updateConfigHashcode(); + + /* Register our update trigger events */ + registerEvents(); + Log.info("Register events"); + + //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.clearTaskQueue(); + + /* Disable core */ + core.disableCore(); + core_enabled = false; + + if (sscache != null) { + sscache.cleanup(); + sscache = null; + } + + Log.info("Disabled"); + } + + // TODO: Clean a bit + public void handleCommand(ServerCommandSource commandSource, String cmd, String[] args) throws CommandSyntaxException { + DynmapCommandSender dsender; + ServerPlayerEntity psender = null; + + // getPlayer throws a CommandSyntaxException, so getEntity and instanceof for safety + if (commandSource.getEntity() instanceof ServerPlayerEntity) { + psender = commandSource.getPlayer(); + } + + if (psender != null) { + // FIXME: New Player? Why not query the current player list. + dsender = new FabricPlayer(this, psender); + } else { + dsender = new FabricCommandSender(commandSource); + } + + core.processCommand(dsender, cmd, cmd, args); + } + + public class PlayerTracker { + public void onPlayerLogin(ServerPlayerEntity player) { + if (!core_enabled) return; + final DynmapPlayer dp = getOrAddPlayer(player); + /* This event can be called from off server thread, so push processing there */ + core.getServer().scheduleServerTask(new Runnable() { + public void run() { + core.listenerManager.processPlayerEvent(DynmapListenerManager.EventType.PLAYER_JOIN, dp); + } + }, 2); + } + + public void onPlayerLogout(ServerPlayerEntity player) { + if (!core_enabled) return; + final DynmapPlayer dp = getOrAddPlayer(player); + final String name = player.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(DynmapListenerManager.EventType.PLAYER_QUIT, dp); + players.remove(name); + } + }, 0); + } + + public void onPlayerChangedDimension(ServerPlayerEntity player) { + if (!core_enabled) return; + getOrAddPlayer(player); // Freshen player object reference + } + + public void onPlayerRespawn(ServerPlayerEntity player) { + if (!core_enabled) return; + getOrAddPlayer(player); // Freshen player object reference + } + } + + private PlayerTracker playerTracker = null; + + private void registerPlayerLoginListener() { + if (playerTracker == null) { + playerTracker = new PlayerTracker(); + PlayerEvents.PLAYER_LOGGED_IN.register(player -> playerTracker.onPlayerLogin(player)); + PlayerEvents.PLAYER_LOGGED_OUT.register(player -> playerTracker.onPlayerLogout(player)); + PlayerEvents.PLAYER_CHANGED_DIMENSION.register(player -> playerTracker.onPlayerChangedDimension(player)); + PlayerEvents.PLAYER_RESPAWN.register(player -> playerTracker.onPlayerRespawn(player)); + } + } + + public class WorldTracker { + public void handleWorldLoad(MinecraftServer server, ServerWorld world) { + if (!core_enabled) return; + + final FabricWorld fw = getWorld(world); + // 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(DynmapListenerManager.EventType.WORLD_LOAD, fw); + } + }, 0); + } + + public void handleWorldUnload(MinecraftServer server, ServerWorld world) { + if (!core_enabled) return; + + final FabricWorld fw = getWorld(world); + 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(DynmapListenerManager.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; + } + } + + public void handleChunkLoad(ServerWorld world, WorldChunk chunk) { + if (!onchunkgenerate) return; + + if ((chunk != null) && (chunk.getStatus() == ChunkStatus.FULL)) { + FabricWorld fw = getWorld(world, false); + if (fw != null) { + addKnownChunk(fw, chunk.getPos()); + } + } + } + + public void handleChunkUnload(ServerWorld world, WorldChunk chunk) { + if (!onchunkgenerate) return; + + if ((chunk != null) && (chunk.getStatus() == ChunkStatus.FULL)) { + FabricWorld fw = getWorld(world, false); + ChunkPos cp = chunk.getPos(); + if (fw != null) { + if (!checkIfKnownChunk(fw, cp)) { + int ymax = 0; + ChunkSection[] sections = chunk.getSectionArray(); + for (int i = 0; i < sections.length; i++) { + if ((sections[i] != null) && (!sections[i].isEmpty())) { + ymax = 16 * (i + 1); + } + } + int x = cp.x << 4; + int z = cp.z << 4; + // If not empty AND not initial scan + if (ymax > 0) { + Log.info("New generated chunk detected at " + cp + " for " + fw.getName()); + mapManager.touchVolume(fw.getName(), x, 0, z, x + 15, ymax, z + 16, "chunkgenerate"); + } + } + removeKnownChunk(fw, cp); + } + } + } + + public void handleChunkDataSave(ServerWorld world, Chunk chunk) { + if (!onchunkgenerate) return; + + if ((chunk != null) && (chunk.getStatus() == ChunkStatus.FULL)) { + FabricWorld fw = getWorld(world, false); + ChunkPos cp = chunk.getPos(); + if (fw != null) { + if (!checkIfKnownChunk(fw, cp)) { + int ymax = 0; + ChunkSection[] sections = chunk.getSectionArray(); + for (int i = 0; i < sections.length; i++) { + if ((sections[i] != null) && (!sections[i].isEmpty())) { + ymax = 16 * (i + 1); + } + } + int x = cp.x << 4; + int z = cp.z << 4; + // If not empty AND not initial scan + if (ymax > 0) { + mapManager.touchVolume(fw.getName(), x, 0, z, x + 15, ymax, z + 16, "chunkgenerate"); + } + addKnownChunk(fw, cp); + } + } + } + } + + public void handleBlockEvent(World world, BlockPos pos) { + if (!core_enabled) return; + if (!onblockchange) return; + if (!(world instanceof ServerWorld)) return; + + BlockUpdateRec r = new BlockUpdateRec(); + r.w = world; + FabricWorld fw = getWorld(world, false); + if (fw == null) return; + r.wid = fw.getName(); + r.x = pos.getX(); + r.y = pos.getY(); + r.z = pos.getZ(); + blockupdatequeue.add(r); + } + } + + private WorldTracker worldTracker = null; + private boolean onblockchange = false; + private boolean onchunkpopulate = false; + private boolean onchunkgenerate = false; + boolean onblockchange_with_id = false; + + private void registerEvents() { + // To trigger rendering. + onblockchange = core.isTrigger("blockupdate"); + onchunkpopulate = core.isTrigger("chunkpopulate"); + onchunkgenerate = core.isTrigger("chunkgenerate"); + onblockchange_with_id = core.isTrigger("blockupdate-with-id"); + if (onblockchange_with_id) + onblockchange = true; + if ((worldTracker == null) && (onblockchange || onchunkpopulate || onchunkgenerate)) { + worldTracker = new WorldTracker(); + ServerWorldEvents.LOAD.register((server, world) -> worldTracker.handleWorldLoad(server, world)); + ServerWorldEvents.UNLOAD.register((server, world) -> worldTracker.handleWorldUnload(server, world)); + ServerChunkEvents.CHUNK_LOAD.register((world, chunk) -> worldTracker.handleChunkLoad(world, chunk)); + ServerChunkEvents.CHUNK_UNLOAD.register((world, chunk) -> worldTracker.handleChunkUnload(world, chunk)); + ChunkDataEvents.SAVE.register((world, chunk) -> worldTracker.handleChunkDataSave(world, chunk)); + BlockEvents.EVENT.register((world, pos) -> worldTracker.handleBlockEvent(world, pos)); + } + // Prime the known full chunks + if (onchunkgenerate && (server.getWorlds() != null)) { + for (ServerWorld world : server.getWorlds()) { + FabricWorld fw = getWorld(world); + if (fw == null) continue; + ServerChunkManager chunkManager = world.getChunkManager(); + Long2ObjectLinkedOpenHashMap chunks = ((ThreadedAnvilChunkStorageAccessor) chunkManager.threadedAnvilChunkStorage).getChunkHolders(); + for (Map.Entry k : chunks.long2ObjectEntrySet()) { + ChunkHolder ch = k.getValue(); + Chunk c = null; + try { + c = ch.getSavingFuture().getNow(null); + } catch (Exception x) { + } + if (c == null) continue; + ChunkStatus cs = c.getStatus(); + ChunkPos pos = ch.getPos(); + if (cs == ChunkStatus.FULL) { // Cooked? + // Add it as known + addKnownChunk(fw, pos); + } + } + } + } + } + + FabricWorld getWorldByName(String name) { + return worlds.get(name); + } + + FabricWorld getWorld(World w) { + return getWorld(w, true); + } + + private FabricWorld getWorld(World w, boolean add_if_not_found) { + if (last_world == w) { + return last_fworld; + } + String wname = FabricWorld.getWorldName(this, w); + + for (FabricWorld fw : worlds.values()) { + if (fw.getRawName().equals(wname)) { + last_world = w; + last_fworld = fw; + if (!fw.isLoaded()) { + fw.setWorldLoaded(w); + } + fw.updateWorld(w); + return fw; + } + } + FabricWorld fw = null; + if (add_if_not_found) { + /* Add to list if not found */ + fw = new FabricWorld(this, w); + worlds.put(fw.getName(), fw); + } + last_world = w; + last_fworld = fw; + return fw; + } + + private void saveWorlds() { + File f = new File(core.getDataFolder(), FabricWorld.SAVED_WORLDS_FILE); + 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("miny", fw.minY); + vals.put("sealevel", fw.sealevel); + vals.put("nether", fw.isNether()); + vals.put("the_end", ((FabricWorld) fw).isTheEnd()); + vals.put("title", fw.getTitle()); + lst.add(vals); + } + cn.put("worlds", lst); + cn.put("useSaveFolderAsName", useSaveFolder); + cn.put("maxWorldHeight", FabricWorld.getMaxWorldHeight()); + + cn.save(); + } + + private void loadWorlds() { + File f = new File(core.getDataFolder(), FabricWorld.SAVED_WORLDS_FILE); + if (f.canRead() == false) { + useSaveFolder = true; + return; + } + ConfigurationNode cn = new ConfigurationNode(f); + cn.load(); + // If defined, use maxWorldHeight + FabricWorld.setMaxWorldHeight(cn.getInteger("maxWorldHeight", 256)); + + // If setting defined, use it + if (cn.containsKey("useSaveFolderAsName")) { + useSaveFolder = cn.getBoolean("useSaveFolderAsName", useSaveFolder); + } + List> lst = cn.getMapList("worlds"); + if (lst == null) { + Log.warning(String.format("Discarding bad %s", FabricWorld.SAVED_WORLDS_FILE)); + return; + } + + for (Map world : lst) { + try { + String name = (String) world.get("name"); + int height = (Integer) world.get("height"); + Integer miny = (Integer) world.get("miny"); + 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) { + FabricWorld fw = new FabricWorld(this, name, height, sealevel, nether, theend, title, (miny != null) ? miny : 0); + fw.setWorldUnloaded(); + core.processWorldLoad(fw); + worlds.put(fw.getName(), fw); + } + } catch (Exception x) { + Log.warning(String.format("Unable to load saved worlds from %s", FabricWorld.SAVED_WORLDS_FILE)); + return; + } + } + } +} + diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricAdapter.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricAdapter.java new file mode 100644 index 00000000..7f809bce --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricAdapter.java @@ -0,0 +1,13 @@ +package org.dynmap.fabric_1_18; + +import net.minecraft.server.world.ServerWorld; +import org.dynmap.DynmapLocation; + +public final class FabricAdapter { + public static DynmapLocation toDynmapLocation(DynmapPlugin plugin, ServerWorld world, double x, double y, double z) { + return new DynmapLocation(plugin.getWorld(world).getName(), x, y, z); + } + + private FabricAdapter() { + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricCommandSender.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricCommandSender.java new file mode 100644 index 00000000..6b001668 --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricCommandSender.java @@ -0,0 +1,47 @@ +package org.dynmap.fabric_1_18; + +import net.minecraft.server.command.ServerCommandSource; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import org.dynmap.common.DynmapCommandSender; + +/* Handler for generic console command sender */ +public class FabricCommandSender implements DynmapCommandSender { + private ServerCommandSource sender; + + protected FabricCommandSender() { + sender = null; + } + + public FabricCommandSender(ServerCommandSource send) { + sender = send; + } + + @Override + public boolean hasPrivilege(String privid) { + return true; + } + + @Override + public void sendMessage(String msg) { + if (sender != null) { + Text ichatcomponent = new LiteralText(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; + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricLogger.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricLogger.java new file mode 100644 index 00000000..10d8c72b --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricLogger.java @@ -0,0 +1,49 @@ +package org.dynmap.fabric_1_18; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dynmap.utils.DynmapLogger; + +public class FabricLogger implements DynmapLogger { + Logger log; + public static final String DM = "[Dynmap] "; + + FabricLogger() { + 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); + } +} \ No newline at end of file diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricMapChunkCache.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricMapChunkCache.java new file mode 100644 index 00000000..76f13727 --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricMapChunkCache.java @@ -0,0 +1,1325 @@ +package org.dynmap.fabric_1_18; + +import net.minecraft.nbt.*; +import net.minecraft.server.world.ServerChunkManager; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.server.world.ThreadedAnvilChunkStorage; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.util.registry.Registry; +import net.minecraft.world.ChunkSerializer; +import net.minecraft.world.World; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.chunk.ChunkManager; +import net.minecraft.world.chunk.ChunkStatus; +import org.dynmap.DynmapChunk; +import org.dynmap.DynmapCore; +import org.dynmap.DynmapWorld; +import org.dynmap.Log; +import org.dynmap.common.BiomeMap; +import org.dynmap.hdmap.HDBlockModels; +import org.dynmap.renderer.DynmapBlockState; +import org.dynmap.renderer.RenderPatchFactory; +import org.dynmap.utils.*; + +import java.lang.reflect.Field; +import java.util.*; + +/** + * Container for managing chunks - dependent upon using chunk snapshots, since rendering is off server thread + */ +public class FabricMapChunkCache extends MapChunkCache { + private static boolean init = false; + private static Field updateEntityTick = null; + /* ChunkManager fields */ + private static Field chunksToRemove = null; // Map + + private World w; + private DynmapWorld dw; + private ServerChunkManager cps; + private int nsect; + private int sectoff; + 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 static final BlockStep unstep[] = {BlockStep.X_MINUS, BlockStep.Y_MINUS, BlockStep.Z_MINUS, + BlockStep.X_PLUS, BlockStep.Y_PLUS, BlockStep.Z_PLUS + }; + + private BiomeMap[] biome_to_bmap; + + private static final int getIndexInChunk(int cx, int cy, int cz) { + return (cy << 8) | (cz << 4) | cx; + } + + /** + * Construct empty cache + */ + public FabricMapChunkCache(DynmapPlugin plugin) { + Registry biomeRegistry = plugin.getFabricServer().getBiomeRegistry(); + Biome b[] = plugin.getFabricServer().getBiomeList(biomeRegistry); + 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; + + //FIXME: This probably not correct. In 1.16.1 it was: b[i].getTranslationKey(); + String bs = biomeRegistry.getId(b[i]).toString(); + + for (int j = 0; j < bm.length; j++) { + if (bm[j].toString().equals(bs)) { + biome_to_bmap[i] = bm[j]; + break; + } + } + } + + init(); + } + + /** + * 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 miny; + 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(); + miny = dw.minY; + } + + @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 >= miny) && (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 < miny) { + 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 < miny) || (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 > miny) { + 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 - miny)) + (y - miny)) << 8) | (bx << 4) | bz; + } + + @Override + public final boolean isEmptySection() { + boolean[] flags = isSectionNotEmpty[chunkindex]; + if(flags == null) { + initSectionData(chunkindex); + flags = isSectionNotEmpty[chunkindex]; + } + return !flags[(y >> 4) + sectoff]; + } + + @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 > 3); + } + } + + 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 = ServerChunkManager.class.getDeclaredFields(); + + f = ServerWorld.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 = ChunkManager.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); +// } + } + if (updateEntityTick == null) { + Log.severe("ERROR: cannot find updateEntityTick - dynmap cannot drive entity cleanup when no players are active"); + } + + init = true; + } + } + + public void setChunks(FabricWorld dw, List chunks) { + this.dw = dw; + this.w = dw.getWorld(); + if (dw.isLoaded()) { + /* Check if world's provider is ServerChunkManager */ + ChunkManager cp = this.w.getChunkManager(); + + if (cp instanceof ServerChunkManager) { + cps = (ServerChunkManager) cp; + } else { + Log.severe("Error: world " + dw.getName() + " has unsupported chunk provider"); + } + } else { + chunks = new ArrayList(); + } + nsect = (dw.worldheight - dw.minY) >> 4; + sectoff = (-dw.minY) >> 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][]; + + } + + public NbtCompound readChunk(int x, int z) { + try { + ThreadedAnvilChunkStorage acl = cps.threadedAnvilChunkStorage; + + ChunkPos coord = new ChunkPos(x, z); + NbtCompound rslt = acl.getNbt(coord); + if (rslt != null) { + // Don't load uncooked chunks + String stat = rslt.getString("Status"); + ChunkStatus cs = ChunkStatus.byId(stat); + if ((stat == null) || + // Needs to be at least lighted + (!cs.isAtLeast(ChunkStatus.LIGHT))) { + 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(NbtElement v) { + Object val = null; + switch (v.getType()) { + case 1: // Byte + val = ((NbtByte) v).byteValue(); + break; + case 2: // Short + val = ((NbtShort) v).shortValue(); + break; + case 3: // Int + val = ((NbtInt) v).intValue(); + break; + case 4: // Long + val = ((NbtLong) v).longValue(); + break; + case 5: // Float + val = ((NbtFloat) v).floatValue(); + break; + case 6: // Double + val = ((NbtDouble) v).doubleValue(); + break; + case 7: // Byte[] + val = ((NbtByteArray) v).getByteArray(); + break; + case 8: // String + val = ((NbtString) v).asString(); + break; + case 9: // List + NbtList tl = (NbtList) v; + ArrayList vlist = new ArrayList(); + int type = tl.getHeldType(); + 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: + NbtCompound 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 + NbtCompound tc = (NbtCompound) v; + HashMap vmap = new HashMap(); + for (Object t : tc.getKeys()) { + String st = (String) t; + NbtElement tg = tc.get(st); + vmap.put(st, getNBTValue(tg)); + } + val = vmap; + break; + case 11: // Int[] + val = ((NbtIntArray) 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; + SnapshotCache.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); + } + + // Prep snapshot and add to cache + private SnapshotCache.SnapshotRec prepChunkSnapshot(DynmapChunk chunk, NbtCompound nbt) throws ChunkSnapshot.StateListException { + ChunkSnapshot ss = new ChunkSnapshot(nbt, dw.worldheight); + DynIntHashMap tileData = new DynIntHashMap(); + + NbtList tiles = nbt.getList("TileEntities", 10); + if (tiles == null) tiles = new NbtList(); + /* Get tile entity data */ + List vals = new ArrayList(); + for (int tid = 0; tid < tiles.size(); tid++) { + NbtCompound 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) { + NbtElement 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); + } + } + } + SnapshotCache.SnapshotRec ssr = new SnapshotCache.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.isChunkLoaded(chunk.x, chunk.z)) { + ChunkSnapshot ss; + DynIntHashMap tileData; + if (vis) { // If visible + NbtCompound nbt = null; + try { + nbt = ChunkSerializer.serialize((ServerWorld) w, cps.getWorldChunk(chunk.x, chunk.z, false)); + } catch (NullPointerException e) { + // TODO: find out why this is happening and why it only seems to happen since 1.16.2 + Log.severe("ChunkSerializer.serialize threw a NullPointerException", e); + continue; + } + try { + SnapshotCache.SnapshotRec ssr = prepChunkSnapshot(chunk, nbt); + ss = ssr.ss; + tileData = ssr.tileData; + } catch (ChunkSnapshot.StateListException e) { + continue; + } + } 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 { + NbtCompound 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 + try { + SnapshotCache.SnapshotRec ssr = prepChunkSnapshot(chunk, nbt); + ss = ssr.ss; + tileData = ssr.tileData; + } catch (ChunkSnapshot.StateListException e) { + continue; + } + } + 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 - sectoff) == false) { + isSectionNotEmpty[idx][i] = true; + } + } + } + } + + public boolean isEmptySection(int sx, int sy, int sz) { + int idx = (sx - x_min) + (sz - z_min) * x_dim; + boolean[] flags = isSectionNotEmpty[idx]; + if (flags == null) { + initSectionData(idx); + flags = isSectionNotEmpty[idx]; + } + return !flags[sy + sectoff]; + } + + /** + * 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; + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricPlayer.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricPlayer.java new file mode 100644 index 00000000..f2b84327 --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricPlayer.java @@ -0,0 +1,252 @@ +package org.dynmap.fabric_1_18; + +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 net.minecraft.network.packet.s2c.play.SubtitleS2CPacket; +import net.minecraft.network.packet.s2c.play.TitleFadeS2CPacket; +import net.minecraft.network.packet.s2c.play.TitleS2CPacket; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.util.Util; +import net.minecraft.util.math.Vec3d; +import org.dynmap.DynmapLocation; +import org.dynmap.common.DynmapPlayer; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.UUID; + +/** + * Player access abstraction class + */ +public class FabricPlayer extends FabricCommandSender implements DynmapPlayer { + private static final Gson GSON = new GsonBuilder().create(); + private final DynmapPlugin plugin; + // FIXME: Proper setter + ServerPlayerEntity player; + private final String skinurl; + private final UUID uuid; + + public FabricPlayer(DynmapPlugin plugin, ServerPlayerEntity player) { + this.plugin = plugin; + this.player = player; + String url = null; + if (this.player != null) { + uuid = this.player.getUuid(); + GameProfile prof = this.player.getGameProfile(); + if (prof != null) { + Property textureProperty = Iterables.getFirst(prof.getProperties().get("textures"), null); + + if (textureProperty != null) { + DynmapPlugin.TexturesPayload result = null; + try { + String json = new String(Base64.getDecoder().decode(textureProperty.getValue()), StandardCharsets.UTF_8); + result = GSON.fromJson(json, DynmapPlugin.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) { + String n = player.getName().getString(); + ; + return n; + } else + return "[Server]"; + } + + @Override + public String getDisplayName() { + if (player != null) { + String n = player.getDisplayName().getString(); + return n; + } else + return "[Server]"; + } + + @Override + public boolean isOnline() { + return true; + } + + @Override + public DynmapLocation getLocation() { + if (player == null) { + return null; + } + + Vec3d pos = player.getPos(); + return FabricAdapter.toDynmapLocation(plugin, player.getWorld(), pos.getX(), pos.getY(), pos.getZ()); + } + + @Override + public String getWorld() { + if (player == null) { + return null; + } + + if (player.world != null) { + return plugin.getWorld(player.world).getName(); + } + + return null; + } + + @Override + public InetSocketAddress getAddress() { + if (player != null) { + ServerPlayNetworkHandler networkHandler = player.networkHandler; + if ((networkHandler != null) && (networkHandler.getConnection() != null)) { + SocketAddress sa = networkHandler.getConnection().getAddress(); + 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.getArmor(); + } 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 plugin.hasPerm(player, privid); + return false; + } + + @Override + public boolean isOp() { + return plugin.isOp(player.getName().getString()); + } + + @Override + public void sendMessage(String msg) { + Text ichatcomponent = new LiteralText(msg); + player.sendSystemMessage(ichatcomponent, Util.NIL_UUID); + } + + @Override + public boolean isInvisible() { + if (player != null) { + return player.isInvisible(); + } + return false; + } + + @Override + public int getSortWeight() { + return plugin.getSortWeight(getName()); + } + + @Override + public void setSortWeight(int wt) { + if (wt == 0) { + plugin.dropSortWeight(getName()); + } else { + plugin.setSortWeight(getName(), wt); + } + } + + @Override + public boolean hasPermissionNode(String node) { + return player != null && plugin.hasPermNode(player, node); + } + + @Override + public String getSkinURL() { + return skinurl; + } + + @Override + public UUID getUUID() { + return uuid; + } + + /** + * Send title and subtitle text (called from server thread) + */ + @Override + public void sendTitleText(String title, String subtitle, int fadeInTicks, int stayTicks, int fadeOutTicks) { + if (player != null) { + ServerPlayerEntity player = this.player; + TitleFadeS2CPacket times = new TitleFadeS2CPacket(fadeInTicks, stayTicks, fadeOutTicks); + player.networkHandler.sendPacket(times); + if (title != null) { + TitleS2CPacket titlepkt = new TitleS2CPacket(new LiteralText(title)); + player.networkHandler.sendPacket(titlepkt); + } + + if (subtitle != null) { + SubtitleS2CPacket subtitlepkt = new SubtitleS2CPacket(new LiteralText(subtitle)); + player.networkHandler.sendPacket(subtitlepkt); + } + } + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricServer.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricServer.java new file mode 100644 index 00000000..eb05ecd4 --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricServer.java @@ -0,0 +1,620 @@ +package org.dynmap.fabric_1_18; + +import com.mojang.authlib.GameProfile; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.ModContainer; +import net.minecraft.block.Block; +import net.minecraft.block.BlockState; +import net.minecraft.network.MessageType; +import net.minecraft.server.BannedIpList; +import net.minecraft.server.BannedPlayerList; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.text.LiteralText; +import net.minecraft.text.Text; +import net.minecraft.util.UserCache; +import net.minecraft.util.Util; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.registry.Registry; +import net.minecraft.world.biome.Biome; +import org.dynmap.DynmapChunk; +import org.dynmap.DynmapCommonAPIListener; +import org.dynmap.DynmapWorld; +import org.dynmap.Log; +import org.dynmap.common.BiomeMap; +import org.dynmap.common.DynmapListenerManager; +import org.dynmap.common.DynmapPlayer; +import org.dynmap.common.DynmapServerInterface; +import org.dynmap.fabric_1_18.event.ServerChatEvents; +import org.dynmap.utils.MapChunkCache; +import org.dynmap.utils.VisibilityLimit; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.Collectors; + +/** + * Server access abstraction class + */ +public class FabricServer extends DynmapServerInterface { + /* Server thread scheduler */ + private final Object schedlock = new Object(); + private final DynmapPlugin plugin; + private final MinecraftServer server; + private final Registry biomeRegistry; + private long cur_tick; + private long next_id; + private long cur_tick_starttime; + private PriorityQueue runqueue = new PriorityQueue(); + + public FabricServer(DynmapPlugin plugin, MinecraftServer server) { + this.plugin = plugin; + this.server = server; + this.biomeRegistry = server.getRegistryManager().get(Registry.BIOME_KEY); + } + + private Optional getProfileByName(String player) { + UserCache cache = server.getUserCache(); + return cache.findByName(player); + } + + public final Registry getBiomeRegistry() { + return biomeRegistry; + } + + private Biome[] biomelist = null; + + public final Biome[] getBiomeList(Registry biomeRegistry) { + if (biomelist == null) { + biomelist = new Biome[256]; + Iterator iter = biomeRegistry.iterator(); + while (iter.hasNext()) { + Biome b = iter.next(); + int bidx = biomeRegistry.getRawId(b); + if (bidx >= biomelist.length) { + biomelist = Arrays.copyOf(biomelist, bidx + biomelist.length); + } + biomelist[bidx] = b; + } + } + return biomelist; + } + + @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) { + /* Add task record to queue */ + synchronized (schedlock) { + TaskRecord tr = new TaskRecord(cur_tick + delay, next_id++, new FutureTask(run, null)); + runqueue.add(tr); + } + } + + @Override + public DynmapPlayer[] getOnlinePlayers() { + if (server.getPlayerManager() == null) return new DynmapPlayer[0]; + + List players = server.getPlayerManager().getPlayerList(); + int playerCount = players.size(); + DynmapPlayer[] dplay = new DynmapPlayer[players.size()]; + + for (int i = 0; i < playerCount; i++) { + ServerPlayerEntity player = players.get(i); + dplay[i] = plugin.getOrAddPlayer(player); + } + + return dplay; + } + + @Override + public void reload() { + plugin.onDisable(); + plugin.onEnable(); + plugin.onStart(); + } + + @Override + public DynmapPlayer getPlayer(String name) { + List players = server.getPlayerManager().getPlayerList(); + + for (ServerPlayerEntity player : players) { + + if (player.getName().getString().equalsIgnoreCase(name)) { + return plugin.getOrAddPlayer(player); + } + } + + return null; + } + + @Override + public Set getIPBans() { + BannedIpList bl = server.getPlayerManager().getIpBanList(); + Set ips = new HashSet(); + + for (String s : bl.getNames()) { + ips.add(s); + } + + return ips; + } + + @Override + public Future callSyncMethod(Callable task) { + return callSyncMethod(task, 0); + } + + public Future callSyncMethod(Callable task, long delay) { + FutureTask ft = new FutureTask(task); + + /* Add task record to queue */ + synchronized (schedlock) { + TaskRecord tr = new TaskRecord(cur_tick + delay, next_id++, ft); + runqueue.add(tr); + } + + return ft; + } + + void clearTaskQueue() { + this.runqueue.clear(); + } + + @Override + public String getServerName() { + String sn; + if (server.isSingleplayer()) + sn = "Integrated"; + else + sn = server.getServerIp(); + if (sn == null) sn = "Unknown Server"; + return sn; + } + + @Override + public boolean isPlayerBanned(String pid) { + PlayerManager scm = server.getPlayerManager(); + BannedPlayerList bl = scm.getUserBanList(); + try { + return bl.contains(getProfileByName(pid).get()); + } catch (NoSuchElementException e) { + /* If this profile doesn't exist, default to "banned" for good measure. */ + return true; + } + } + + @Override + public String stripChatColor(String s) { + return DynmapPlugin.patternControlCode.matcher(s).replaceAll(""); + } + + private Set registered = new HashSet(); + + @Override + public boolean requestEventNotification(DynmapListenerManager.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 (plugin.chathandler == null) { + plugin.setChatHandler(new DynmapPlugin.ChatHandler(plugin)); + ServerChatEvents.EVENT.register((player, message) -> plugin.chathandler.handleChat(player, message)); + } + 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) { + Text component = new LiteralText(msg); + server.getPlayerManager().broadcast(component, MessageType.SYSTEM, Util.NIL_UUID); + 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 (plugin.sscache != null) + return plugin.sscache.getHitRate(); + return 0.0; + } + + @Override + public void resetCacheStats() { + if (plugin.sscache != null) + plugin.sscache.resetStats(); + } + + @Override + public DynmapWorld getWorldByName(String wname) { + return plugin.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) { + if (isPlayerBanned(player)) { + return Collections.emptySet(); + } + Set rslt = plugin.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) { + if (isPlayerBanned(player)) { + return false; + } + return plugin.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) { + FabricMapChunkCache c = (FabricMapChunkCache) 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)) { + 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 FabricMapChunkCache cc = c; + Future f = this.callSyncMethod(new Callable() { + public Boolean call() throws Exception { + // Update busy state on world + //FabricWorld fw = (FabricWorld) 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()) { + return null; + } + // Now, do rest of chunk reading from calling thread + c.readChunks(chunks.size()); + + return c; + } + + @Override + public int getMaxPlayers() { + return server.getMaxPlayerCount(); + } + + @Override + public int getCurrentPlayers() { + return server.getPlayerManager().getCurrentPlayerCount(); + } + + public void tickEvent(MinecraftServer server) { + cur_tick_starttime = System.nanoTime(); + long elapsed = cur_tick_starttime - plugin.lasttick; + plugin.lasttick = cur_tick_starttime; + plugin.avgticklen = ((plugin.avgticklen * 99) / 100) + (elapsed / 100); + plugin.tps = (double) 1E9 / (double) plugin.avgticklen; + // Tick core + if (plugin.core != null) { + plugin.core.serverTick(plugin.tps); + } + + boolean done = false; + TaskRecord tr = null; + + while (!plugin.blockupdatequeue.isEmpty()) { + DynmapPlugin.BlockUpdateRec r = plugin.blockupdatequeue.remove(); + BlockState bs = r.w.getBlockState(new BlockPos(r.x, r.y, r.z)); + int idx = Block.STATE_IDS.getRawId(bs); + if (!org.dynmap.hdmap.HDBlockModels.isChangeIgnoredBlock(DynmapPlugin.stateByID[idx])) { + if (plugin.onblockchange_with_id) + plugin.mapManager.touch(r.wid, r.x, r.y, r.z, "blockchange[" + idx + "]"); + else + plugin.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.getTickToRun() > cur_tick) || ((now - cur_tick_starttime) > plugin.perTickLimit)) { + done = true; + } else { + tr = runqueue.poll(); + } + } + while (!done) { + tr.run(); + + synchronized (schedlock) { + tr = runqueue.peek(); + now = System.nanoTime(); + /* Nothing due to run */ + if ((tr == null) || (tr.getTickToRun() > cur_tick) || ((now - cur_tick_starttime) > plugin.perTickLimit)) { + done = true; + } else { + tr = runqueue.poll(); + } + } + } + while (!plugin.msgqueue.isEmpty()) { + DynmapPlugin.ChatMessage cm = plugin.msgqueue.poll(); + DynmapPlayer dp = null; + if (cm.sender != null) + dp = plugin.getOrAddPlayer(cm.sender); + else + dp = new FabricPlayer(plugin, null); + + plugin.core.listenerManager.processChatEvent(DynmapListenerManager.EventType.PLAYER_CHAT, dp, cm.message); + } + // Check for generated chunks + if ((cur_tick % 20) == 0) { + } + } + + private Optional getModContainerById(String id) { + return FabricLoader.getInstance().getModContainer(id); + } + + @Override + public boolean isModLoaded(String name) { + return FabricLoader.getInstance().getModContainer(name).isPresent(); + } + + @Override + public String getModVersion(String name) { + Optional mod = getModContainerById(name); // Try case sensitive lookup + return mod.map(modContainer -> modContainer.getMetadata().getVersion().getFriendlyString()).orElse(null); + } + + @Override + public double getServerTPS() { + return plugin.tps; + } + + @Override + public String getServerIP() { + if (server.isSingleplayer()) + return "0.0.0.0"; + else + return server.getServerIp(); + } + + @Override + public File getModContainerFile(String name) { + Optional container = getModContainerById(name); // Try case sensitive lookup + if (container.isPresent()) { + Path path = container.get().getRootPath(); + if (path.getFileSystem().provider().getScheme().equals("jar")) { + path = Paths.get(path.getFileSystem().toString()); + } + return path.toFile(); + } + return null; + } + + @Override + public List getModList() { + return FabricLoader.getInstance() + .getAllMods() + .stream() + .map(container -> container.getMetadata().getId()) + .collect(Collectors.toList()); + } + + @Override + public Map getBlockIDMap() { + Map map = new HashMap(); + return map; + } + + @Override + public InputStream openResource(String modid, String rname) { + if (modid == null) modid = "minecraft"; + + if ("minecraft".equals(modid)) { + return MinecraftServer.class.getClassLoader().getResourceAsStream(rname); + } else { + if (rname.startsWith("/") || rname.startsWith("\\")) { + rname = rname.substring(1); + } + + final String finalModid = modid; + final String finalRname = rname; + return getModContainerById(modid).map(container -> { + try { + return Files.newInputStream(container.getPath(finalRname)); + } catch (IOException e) { + Log.severe("Failed to load resource of mod :" + finalModid, e); + return null; + } + }).orElse(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; + } + +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricWorld.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricWorld.java new file mode 100644 index 00000000..c8b61edb --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/FabricWorld.java @@ -0,0 +1,229 @@ +package org.dynmap.fabric_1_18; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.registry.RegistryKey; +import net.minecraft.world.Heightmap; +import net.minecraft.world.LightType; +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; + +import java.util.List; + +public class FabricWorld extends DynmapWorld { + // TODO: Store this relative to World saves for integrated server + public static final String SAVED_WORLDS_FILE = "fabricworlds.yml"; + + private final DynmapPlugin plugin; + private World world; + private final boolean skylight; + private final boolean isnether; + private final boolean istheend; + private final String env; + private DynmapLocation spawnloc = new DynmapLocation(); + private static int maxWorldHeight = 320; // Maximum allows world height + + public static int getMaxWorldHeight() { + return maxWorldHeight; + } + + public static void setMaxWorldHeight(int h) { + maxWorldHeight = h; + } + + public static String getWorldName(DynmapPlugin plugin, World w) { + RegistryKey rk = w.getRegistryKey(); + if (rk == World.OVERWORLD) { // Overworld? + return w.getServer().getSaveProperties().getLevelName(); + } else if (rk == World.END) { + return "DIM1"; + } else if (rk == World.NETHER) { + return "DIM-1"; + } else { + return rk.getValue().getNamespace() + "_" + rk.getValue().getPath(); + } + } + + public void updateWorld(World w) { + this.updateWorldHeights(w.getHeight(), w.getDimension().getMinimumY(), w.getSeaLevel()); + } + + public FabricWorld(DynmapPlugin plugin, World w) { + this(plugin, getWorldName(plugin, w), w.getHeight(), + w.getSeaLevel(), + w.getRegistryKey() == World.NETHER, + w.getRegistryKey() == World.END, + w.getRegistryKey().getValue().getPath(), + w.getDimension().getMinimumY()); + setWorldLoaded(w); + } + + public FabricWorld(DynmapPlugin plugin, String name, int height, int sealevel, boolean nether, boolean the_end, String deftitle, int miny) { + super(name, (height > maxWorldHeight) ? maxWorldHeight : height, sealevel, miny); + this.plugin = plugin; + 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) { + spawnloc.x = world.getLevelProperties().getSpawnX(); + spawnloc.y = world.getLevelProperties().getSpawnY(); + spawnloc.z = world.getLevelProperties().getSpawnZ(); + spawnloc.world = this.getName(); + } + return spawnloc; + } + + /* Get world time */ + @Override + public long getTime() { + if (world != null) + return world.getTime(); + else + return -1; + } + + /* World is storming */ + @Override + public boolean hasStorm() { + if (world != null) + return world.isRaining(); + else + return false; + } + + /* World is thundering */ + @Override + public boolean isThundering() { + if (world != null) + return world.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(World w) { + world = w; + this.sealevel = w.getSeaLevel(); // Read actual current sealevel from world + // Update lighting table + for (int i = 0; i < 16; i++) { + this.setBrightnessTableEntry(i, w.getDimension().getBrightness(i)); + } + } + + /* Get light level of block */ + @Override + public int getLightLevel(int x, int y, int z) { + if (world != null) + return world.getLightLevel(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.getChunk(x >> 4, z >> 4).getHeightmap(Heightmap.Type.MOTION_BLOCKING).get(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.getLightLevel(LightType.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) { + FabricMapChunkCache c = new FabricMapChunkCache(plugin); + c.setChunks(this, chunks); + return c; + } + return null; + } + + public World getWorld() { + return world; + } + + @Override + public Polygon getWorldBorder() { + if (world != null) { + WorldBorder wb = world.getWorldBorder(); + if ((wb != null) && (wb.getSize() < 5.9E7)) { + Polygon p = new Polygon(); + p.addVertex(wb.getBoundWest(), wb.getBoundNorth()); + p.addVertex(wb.getBoundWest(), wb.getBoundSouth()); + p.addVertex(wb.getBoundEast(), wb.getBoundSouth()); + p.addVertex(wb.getBoundEast(), wb.getBoundNorth()); + return p; + } + } + return null; + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/SnapshotCache.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/SnapshotCache.java new file mode 100644 index 00000000..f2ef9f35 --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/SnapshotCache.java @@ -0,0 +1,201 @@ +package org.dynmap.fabric_1_18; + +import org.dynmap.utils.DynIntHashMap; + +import java.lang.ref.Reference; +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +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/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/TaskRecord.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/TaskRecord.java new file mode 100644 index 00000000..6c67edf8 --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/TaskRecord.java @@ -0,0 +1,38 @@ +package org.dynmap.fabric_1_18; + +import java.util.concurrent.FutureTask; + +class TaskRecord implements Comparable { + TaskRecord(long ticktorun, long id, FutureTask future) { + this.ticktorun = ticktorun; + this.id = id; + this.future = future; + } + + private final long ticktorun; + private final long id; + private final FutureTask future; + + void run() { + this.future.run(); + } + + long getTickToRun() { + return this.ticktorun; + } + + @Override + public int compareTo(TaskRecord o) { + if (this.ticktorun < o.ticktorun) { + return -1; + } else if (this.ticktorun > o.ticktorun) { + return 1; + } else if (this.id < o.id) { + return -1; + } else if (this.id > o.id) { + return 1; + } else { + return 0; + } + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/VersionCheck.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/VersionCheck.java new file mode 100644 index 00000000..c1639e39 --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/VersionCheck.java @@ -0,0 +1,98 @@ +package org.dynmap.fabric_1_18; + +import org.dynmap.DynmapCore; +import org.dynmap.Log; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; + +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/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/command/DmapCommand.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/command/DmapCommand.java new file mode 100644 index 00000000..7c60466f --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/command/DmapCommand.java @@ -0,0 +1,9 @@ +package org.dynmap.fabric_1_18.command; + +import org.dynmap.fabric_1_18.DynmapPlugin; + +public class DmapCommand extends DynmapCommandExecutor { + public DmapCommand(DynmapPlugin p) { + super("dmap", p); + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/command/DmarkerCommand.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/command/DmarkerCommand.java new file mode 100644 index 00000000..4369dfc4 --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/command/DmarkerCommand.java @@ -0,0 +1,9 @@ +package org.dynmap.fabric_1_18.command; + +import org.dynmap.fabric_1_18.DynmapPlugin; + +public class DmarkerCommand extends DynmapCommandExecutor { + public DmarkerCommand(DynmapPlugin p) { + super("dmarker", p); + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/command/DynmapCommand.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/command/DynmapCommand.java new file mode 100644 index 00000000..efae58a5 --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/command/DynmapCommand.java @@ -0,0 +1,9 @@ +package org.dynmap.fabric_1_18.command; + +import org.dynmap.fabric_1_18.DynmapPlugin; + +public class DynmapCommand extends DynmapCommandExecutor { + public DynmapCommand(DynmapPlugin p) { + super("dynmap", p); + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/command/DynmapCommandExecutor.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/command/DynmapCommandExecutor.java new file mode 100644 index 00000000..e60b35f1 --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/command/DynmapCommandExecutor.java @@ -0,0 +1,63 @@ +package org.dynmap.fabric_1_18.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.tree.ArgumentCommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.mojang.brigadier.tree.RootCommandNode; +import net.minecraft.server.command.ServerCommandSource; +import org.dynmap.fabric_1_18.DynmapPlugin; + +import java.util.Arrays; + +import static com.mojang.brigadier.arguments.StringArgumentType.greedyString; +import static net.minecraft.server.command.CommandManager.argument; +import static net.minecraft.server.command.CommandManager.literal; + +public class DynmapCommandExecutor implements Command { + private final String cmd; + private final DynmapPlugin plugin; + + DynmapCommandExecutor(String cmd, DynmapPlugin plugin) { + this.cmd = cmd; + this.plugin = plugin; + } + + public void register(CommandDispatcher dispatcher) { + final RootCommandNode root = dispatcher.getRoot(); + + final LiteralCommandNode command = literal(this.cmd) + .executes(this) + .build(); + + final ArgumentCommandNode args = argument("args", greedyString()) + .executes(this) + .build(); + + // So this becomes "cmd" [args] + command.addChild(args); + + // Add command to the command dispatcher via root node. + root.addChild(command); + } + + @Override + public int run(CommandContext context) throws CommandSyntaxException { + // Commands in brigadier may be proxied in Minecraft via a syntax like `/execute ... ... run dmap [args]` + // Dynmap will fail to parse this properly, so we find the starting position of the actual command being parsed after any forks or redirects. + // The start position of the range specifies where the actual command dynmap has registered starts + int start = context.getRange().getStart(); + String dynmapInput = context.getInput().substring(start); + + String[] args = dynmapInput.split("\\s+"); + plugin.handleCommand(context.getSource(), cmd, Arrays.copyOfRange(args, 1, args.length)); + return 1; + } + + // @Override // TODO: Usage? + public String getUsage(ServerCommandSource commandSource) { + return "Run /" + cmd + " help for details on using command"; + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/command/DynmapExpCommand.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/command/DynmapExpCommand.java new file mode 100644 index 00000000..3eac643e --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/command/DynmapExpCommand.java @@ -0,0 +1,9 @@ +package org.dynmap.fabric_1_18.command; + +import org.dynmap.fabric_1_18.DynmapPlugin; + +public class DynmapExpCommand extends DynmapCommandExecutor { + public DynmapExpCommand(DynmapPlugin p) { + super("dynmapexp", p); + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/event/BlockEvents.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/event/BlockEvents.java new file mode 100644 index 00000000..f50c1cb9 --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/event/BlockEvents.java @@ -0,0 +1,23 @@ +package org.dynmap.fabric_1_18.event; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +public class BlockEvents { + private BlockEvents() { + } + + public static Event EVENT = EventFactory.createArrayBacked(BlockCallback.class, + (listeners) -> (world, pos) -> { + for (BlockCallback callback : listeners) { + callback.onBlockEvent(world, pos); + } + } + ); + + public interface BlockCallback { + void onBlockEvent(World world, BlockPos pos); + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/event/ChunkDataEvents.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/event/ChunkDataEvents.java new file mode 100644 index 00000000..d50f50b8 --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/event/ChunkDataEvents.java @@ -0,0 +1,24 @@ +package org.dynmap.fabric_1_18.event; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.world.chunk.Chunk; + +public class ChunkDataEvents { + private ChunkDataEvents() { + } + + public static Event SAVE = EventFactory.createArrayBacked(Save.class, + (listeners) -> (world, chunk) -> { + for (Save callback : listeners) { + callback.onSave(world, chunk); + } + } + ); + + @FunctionalInterface + public interface Save { + void onSave(ServerWorld world, Chunk chunk); + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/event/CustomServerLifecycleEvents.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/event/CustomServerLifecycleEvents.java new file mode 100644 index 00000000..d05e841e --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/event/CustomServerLifecycleEvents.java @@ -0,0 +1,14 @@ +package org.dynmap.fabric_1_18.event; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; + +public class CustomServerLifecycleEvents { + public static final Event SERVER_STARTED_PRE_WORLD_LOAD = + EventFactory.createArrayBacked(ServerLifecycleEvents.ServerStarted.class, (callbacks) -> (server) -> { + for (ServerLifecycleEvents.ServerStarted callback : callbacks) { + callback.onServerStarted(server); + } + }); +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/event/PlayerEvents.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/event/PlayerEvents.java new file mode 100644 index 00000000..cb60625e --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/event/PlayerEvents.java @@ -0,0 +1,62 @@ +package org.dynmap.fabric_1_18.event; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.network.ServerPlayerEntity; + +public class PlayerEvents { + private PlayerEvents() { + } + + public static Event PLAYER_LOGGED_IN = EventFactory.createArrayBacked(PlayerLoggedIn.class, + (listeners) -> (player) -> { + for (PlayerLoggedIn callback : listeners) { + callback.onPlayerLoggedIn(player); + } + } + ); + + public static Event PLAYER_LOGGED_OUT = EventFactory.createArrayBacked(PlayerLoggedOut.class, + (listeners) -> (player) -> { + for (PlayerLoggedOut callback : listeners) { + callback.onPlayerLoggedOut(player); + } + } + ); + + public static Event PLAYER_CHANGED_DIMENSION = EventFactory.createArrayBacked(PlayerChangedDimension.class, + (listeners) -> (player) -> { + for (PlayerChangedDimension callback : listeners) { + callback.onPlayerChangedDimension(player); + } + } + ); + + public static Event PLAYER_RESPAWN = EventFactory.createArrayBacked(PlayerRespawn.class, + (listeners) -> (player) -> { + for (PlayerRespawn callback : listeners) { + callback.onPlayerRespawn(player); + } + } + ); + + @FunctionalInterface + public interface PlayerLoggedIn { + void onPlayerLoggedIn(ServerPlayerEntity player); + } + + @FunctionalInterface + public interface PlayerLoggedOut { + void onPlayerLoggedOut(ServerPlayerEntity player); + } + + @FunctionalInterface + public interface PlayerChangedDimension { + void onPlayerChangedDimension(ServerPlayerEntity player); + } + + @FunctionalInterface + public interface PlayerRespawn { + void onPlayerRespawn(ServerPlayerEntity player); + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/event/ServerChatEvents.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/event/ServerChatEvents.java new file mode 100644 index 00000000..71506135 --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/event/ServerChatEvents.java @@ -0,0 +1,23 @@ +package org.dynmap.fabric_1_18.event; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.server.network.ServerPlayerEntity; + +public class ServerChatEvents { + private ServerChatEvents() { + } + + public static Event EVENT = EventFactory.createArrayBacked(ServerChatCallback.class, + (listeners) -> (player, message) -> { + for (ServerChatCallback callback : listeners) { + callback.onChatMessage(player, message); + } + } + ); + + @FunctionalInterface + public interface ServerChatCallback { + void onChatMessage(ServerPlayerEntity player, String message); + } +} \ No newline at end of file diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/BiomeEffectsAccessor.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/BiomeEffectsAccessor.java new file mode 100644 index 00000000..435c226f --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/BiomeEffectsAccessor.java @@ -0,0 +1,11 @@ +package org.dynmap.fabric_1_18.mixin; + +import net.minecraft.world.biome.BiomeEffects; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(BiomeEffects.class) +public interface BiomeEffectsAccessor { + @Accessor + int getWaterColor(); +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/MinecraftServerMixin.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/MinecraftServerMixin.java new file mode 100644 index 00000000..d0cf770a --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/MinecraftServerMixin.java @@ -0,0 +1,16 @@ +package org.dynmap.fabric_1_18.mixin; + +import net.minecraft.server.MinecraftServer; +import org.dynmap.fabric_1_18.event.CustomServerLifecycleEvents; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MinecraftServer.class) +public class MinecraftServerMixin { + @Inject(method = "loadWorld", at = @At("HEAD")) + protected void loadWorld(CallbackInfo info) { + CustomServerLifecycleEvents.SERVER_STARTED_PRE_WORLD_LOAD.invoker().onServerStarted((MinecraftServer) (Object) this); + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/PlayerManagerMixin.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/PlayerManagerMixin.java new file mode 100644 index 00000000..9b5450bc --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/PlayerManagerMixin.java @@ -0,0 +1,29 @@ +package org.dynmap.fabric_1_18.mixin; + +import net.minecraft.network.ClientConnection; +import net.minecraft.server.PlayerManager; +import net.minecraft.server.network.ServerPlayerEntity; +import org.dynmap.fabric_1_18.event.PlayerEvents; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(PlayerManager.class) +public class PlayerManagerMixin { + @Inject(method = "onPlayerConnect", at = @At("TAIL")) + public void onPlayerConnect(ClientConnection connection, ServerPlayerEntity player, CallbackInfo info) { + PlayerEvents.PLAYER_LOGGED_IN.invoker().onPlayerLoggedIn(player); + } + + @Inject(method = "remove", at = @At("HEAD")) + public void remove(ServerPlayerEntity player, CallbackInfo info) { + PlayerEvents.PLAYER_LOGGED_OUT.invoker().onPlayerLoggedOut(player); + } + + @Inject(method = "respawnPlayer", at = @At("RETURN")) + public void respawnPlayer(ServerPlayerEntity player, boolean alive, CallbackInfoReturnable info) { + PlayerEvents.PLAYER_RESPAWN.invoker().onPlayerRespawn(info.getReturnValue()); + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/ServerPlayNetworkHandlerMixin.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/ServerPlayNetworkHandlerMixin.java new file mode 100644 index 00000000..26b8c79e --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/ServerPlayNetworkHandlerMixin.java @@ -0,0 +1,29 @@ +package org.dynmap.fabric_1_18.mixin; + +import net.minecraft.server.filter.TextStream; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import org.dynmap.fabric_1_18.event.ServerChatEvents; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerPlayNetworkHandler.class) +public abstract class ServerPlayNetworkHandlerMixin { + @Shadow + public ServerPlayerEntity player; + + @Inject( + method = "handleMessage", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/server/PlayerManager;broadcast(Lnet/minecraft/text/Text;Ljava/util/function/Function;Lnet/minecraft/network/MessageType;Ljava/util/UUID;)V", + shift = At.Shift.BEFORE + ) + ) + public void onGameMessage(TextStream.Message message, CallbackInfo info) { + ServerChatEvents.EVENT.invoker().onChatMessage(player, message.getRaw()); + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/ServerPlayerEntityMixin.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/ServerPlayerEntityMixin.java new file mode 100644 index 00000000..4ca3d449 --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/ServerPlayerEntityMixin.java @@ -0,0 +1,30 @@ +package org.dynmap.fabric_1_18.mixin; + +import net.minecraft.entity.Entity; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import org.dynmap.fabric_1_18.event.PlayerEvents; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ServerPlayerEntity.class) +public class ServerPlayerEntityMixin { + @Inject(method = "teleport", at = @At("RETURN")) + public void teleport(ServerWorld targetWorld, double x, double y, double z, float yaw, float pitch, CallbackInfo info) { + ServerPlayerEntity player = (ServerPlayerEntity) (Object) this; + if (targetWorld != player.world) { + PlayerEvents.PLAYER_CHANGED_DIMENSION.invoker().onPlayerChangedDimension(player); + } + } + + @Inject(method = "moveToWorld", at = @At("RETURN")) + public void moveToWorld(ServerWorld destination, CallbackInfoReturnable info) { + ServerPlayerEntity player = (ServerPlayerEntity) (Object) this; + if (player.getRemovalReason() == null) { + PlayerEvents.PLAYER_CHANGED_DIMENSION.invoker().onPlayerChangedDimension(player); + } + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/ThreadedAnvilChunkStorageAccessor.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/ThreadedAnvilChunkStorageAccessor.java new file mode 100644 index 00000000..3cb7c9c3 --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/ThreadedAnvilChunkStorageAccessor.java @@ -0,0 +1,13 @@ +package org.dynmap.fabric_1_18.mixin; + +import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; +import net.minecraft.server.world.ChunkHolder; +import net.minecraft.server.world.ThreadedAnvilChunkStorage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ThreadedAnvilChunkStorage.class) +public interface ThreadedAnvilChunkStorageAccessor { + @Accessor + Long2ObjectLinkedOpenHashMap getChunkHolders(); +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/ThreadedAnvilChunkStorageMixin.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/ThreadedAnvilChunkStorageMixin.java new file mode 100644 index 00000000..cecee77d --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/ThreadedAnvilChunkStorageMixin.java @@ -0,0 +1,26 @@ +package org.dynmap.fabric_1_18.mixin; + +import net.minecraft.server.world.ServerWorld; +import net.minecraft.server.world.ThreadedAnvilChunkStorage; +import net.minecraft.world.chunk.Chunk; +import org.dynmap.fabric_1_18.event.ChunkDataEvents; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ThreadedAnvilChunkStorage.class) +public abstract class ThreadedAnvilChunkStorageMixin { + @Shadow + @Final + private ServerWorld world; + + @Inject(method = "save(Lnet/minecraft/world/chunk/Chunk;)Z", at = @At("RETURN")) + private void save(Chunk chunk, CallbackInfoReturnable info) { + if (info.getReturnValueZ()) { + ChunkDataEvents.SAVE.invoker().onSave(this.world, chunk); + } + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/WorldChunkMixin.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/WorldChunkMixin.java new file mode 100644 index 00000000..b59ce04d --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/mixin/WorldChunkMixin.java @@ -0,0 +1,25 @@ +package org.dynmap.fabric_1_18.mixin; + +import net.minecraft.block.BlockState; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraft.world.chunk.WorldChunk; +import org.dynmap.fabric_1_18.event.BlockEvents; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(WorldChunk.class) +public abstract class WorldChunkMixin { + @Shadow + public abstract World getWorld(); + + @Inject(method = "setBlockState", at = @At("RETURN")) + public void setBlockState(BlockPos pos, BlockState state, boolean moved, CallbackInfoReturnable info) { + if (info.getReturnValue() != null) { + BlockEvents.EVENT.invoker().onBlockEvent(this.getWorld(), pos); + } + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/permissions/FilePermissions.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/permissions/FilePermissions.java new file mode 100644 index 00000000..eaf1c421 --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/permissions/FilePermissions.java @@ -0,0 +1,103 @@ +package org.dynmap.fabric_1_18.permissions; + +import net.minecraft.entity.player.PlayerEntity; +import org.dynmap.ConfigurationNode; +import org.dynmap.Log; +import org.dynmap.fabric_1_18.DynmapPlugin; + +import java.io.File; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +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(PlayerEntity psender, String permission) { + if (psender != null) { + String n = psender.getName().getString().toLowerCase(); + return hasPerm(n, permission); + } + return true; + } + + @Override + public boolean hasPermissionNode(PlayerEntity psender, String permission) { + if (psender != null) { + String player = psender.getName().getString().toLowerCase(); + return DynmapPlugin.plugin.isOp(player); + } + return false; + } + +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/permissions/OpPermissions.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/permissions/OpPermissions.java new file mode 100644 index 00000000..73d8688f --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/permissions/OpPermissions.java @@ -0,0 +1,52 @@ +package org.dynmap.fabric_1_18.permissions; + +import net.minecraft.entity.player.PlayerEntity; +import org.dynmap.Log; +import org.dynmap.fabric_1_18.DynmapPlugin; + +import java.util.HashSet; +import java.util.Set; + +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(PlayerEntity psender, String permission) { + if (psender != null) { + if (usrCommands.contains(permission)) { + return true; + } + return DynmapPlugin.plugin.isOp(psender.getName().getString()); + } + return true; + } + + @Override + public boolean hasPermissionNode(PlayerEntity psender, String permission) { + if (psender != null) { + return DynmapPlugin.plugin.isOp(psender.getName().getString()); + } + return true; + } +} diff --git a/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/permissions/PermissionProvider.java b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/permissions/PermissionProvider.java new file mode 100644 index 00000000..4dbdabff --- /dev/null +++ b/fabric-1.18/src/main/java/org/dynmap/fabric_1_18/permissions/PermissionProvider.java @@ -0,0 +1,16 @@ +package org.dynmap.fabric_1_18.permissions; + +import net.minecraft.entity.player.PlayerEntity; + +import java.util.Set; + +public interface PermissionProvider { + boolean has(PlayerEntity sender, String permission); + + boolean hasPermissionNode(PlayerEntity sender, String permission); + + Set hasOfflinePermissions(String player, Set perms); + + boolean hasOfflinePermission(String player, String perm); + +} diff --git a/fabric-1.18/src/main/resources/assets/dynmap/icon.png b/fabric-1.18/src/main/resources/assets/dynmap/icon.png new file mode 100644 index 00000000..d18f3e14 Binary files /dev/null and b/fabric-1.18/src/main/resources/assets/dynmap/icon.png differ diff --git a/fabric-1.18/src/main/resources/configuration.txt b/fabric-1.18/src/main/resources/configuration.txt new file mode 100644 index 00000000..13dc64a3 --- /dev/null +++ b/fabric-1.18/src/main/resources/configuration.txt @@ -0,0 +1,467 @@ +# 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 +# mariadb: MariaDB database, at hostname:port in database, accessed via userid with password +# postgres: PostgreSQL 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 names using the textures and models of another block name: 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-alias: +# "minecraft:quartz_ore": "stone" +# "diamond_ore": "coal_ore" + +# Default image format for HDMaps (png, jpg, jpg-q75, jpg-q80, jpg-q85, jpg-q90, jpg-q95, jpg-q100, webp, webp-q75, webp-q80, webp-q85, webp-q90, webp-q95, webp-q100, webp-l), +# Note: any webp format requires the presence of the 'webp command line tools' (cwebp, dwebp) (https://developers.google.com/speed/webp/download) +# +# Has no effect on maps with explicit format settings +image-format: jpg-q90 + +# If cwebp or dwebp are not on the PATH, use these settings to provide their full path. Do not use these settings if the tools are on the PATH +# For Windows, include .exe +# +#cwebpPath: /usr/bin/cwebp +#dwebpPath: /usr/bin/dwebp + +# 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 + +# Player enter/exit title messages for map markers +# +# Processing period - how often to check player positions vs markers - default is 1000ms (1 second) +#enterexitperiod: 1000 +# Title message fade in time, in ticks (0.05 second intervals) - default is 10 (1/2 second) +#titleFadeIn: 10 +# Title message stay time, in ticks (0.05 second intervals) - default is 70 (3.5 seconds) +#titleStay: 70 +# Title message fade out time, in ticks (0.05 seocnd intervals) - default is 20 (1 second) +#titleFadeOut: 20 +# Enter/exit messages use on screen titles (true - default), if false chat messages are sent instead +#enterexitUseTitle: true +# Set true if new enter messages should supercede pending exit messages (vs being queued in order), default false +#enterReplacesExits: 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/fabric-1.18/src/main/resources/dynmap.mixins.json b/fabric-1.18/src/main/resources/dynmap.mixins.json new file mode 100644 index 00000000..9e8d2f97 --- /dev/null +++ b/fabric-1.18/src/main/resources/dynmap.mixins.json @@ -0,0 +1,19 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "org.dynmap.fabric_1_18.mixin", + "compatibilityLevel": "JAVA_16", + "mixins": [ + "BiomeEffectsAccessor", + "MinecraftServerMixin", + "PlayerManagerMixin", + "ServerPlayerEntityMixin", + "ServerPlayNetworkHandlerMixin", + "ThreadedAnvilChunkStorageAccessor", + "ThreadedAnvilChunkStorageMixin", + "WorldChunkMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/fabric-1.18/src/main/resources/fabric.mod.json b/fabric-1.18/src/main/resources/fabric.mod.json new file mode 100644 index 00000000..7d13f85e --- /dev/null +++ b/fabric-1.18/src/main/resources/fabric.mod.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 1, + "id": "dynmap", + "version": "${version}", + "name": "Dynmap", + "description": "Dynamic, Google-maps style rendered maps for your Minecraft server", + "authors": [ + "mikeprimm", + "LolHens", + "i509VCB" + ], + "contact": { + "homepage": "https://github.com/webbukkit/dynmap", + "sources": "https://github.com/webbukkit/dynmap" + }, + "license": "Apache-2.0", + "icon": "assets/dynmap/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + "org.dynmap.fabric_1_18.DynmapMod" + ] + }, + "mixins": [ + "dynmap.mixins.json" + ], + + "depends": { + "fabricloader": ">=0.12.6", + "fabric": ">=0.43.1", + "minecraft": "1.18.x" + } +} diff --git a/fabric-1.18/src/main/resources/permissions.yml.example b/fabric-1.18/src/main/resources/permissions.yml.example new file mode 100644 index 00000000..a25f9adc --- /dev/null +++ b/fabric-1.18/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/settings.gradle b/settings.gradle index 5e662776..1709e67d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,6 +21,7 @@ include ':bukkit-helper' include ':dynmap-api' include ':DynmapCore' include ':DynmapCoreAPI' +include ':fabric-1.18' include ':fabric-1.17.1' include ':fabric-1.16.4' include ':fabric-1.15.2' @@ -45,6 +46,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(':fabric-1.18').projectDir = "$rootDir/fabric-1.18" as File project(':fabric-1.17.1').projectDir = "$rootDir/fabric-1.17.1" as File project(':fabric-1.16.4').projectDir = "$rootDir/fabric-1.16.4" as File project(':fabric-1.15.2').projectDir = "$rootDir/fabric-1.15.2" as File