Skip to content

Instantly share code, notes, and snippets.

@Commoble
Last active April 6, 2024 12:16
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Commoble/7db2ef25f94952a4d2e2b7e3d4be53e0 to your computer and use it in GitHub Desktop.
Save Commoble/7db2ef25f94952a4d2e2b7e3d4be53e0 to your computer and use it in GitHub Desktop.
Dynamic Dimensions and How to Go to Dimensions in Minecraft Forge 1.16.4
package commoble.hyperbox;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.function.BiFunction;
import java.util.function.Function;
import com.google.common.collect.ImmutableList;
import com.mojang.serialization.Lifecycle;
import net.minecraft.entity.player.ServerPlayerEntity;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.RegistryKey;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.vector.Vector3d;
import net.minecraft.util.registry.Registry;
import net.minecraft.world.Dimension;
import net.minecraft.world.World;
import net.minecraft.world.biome.BiomeManager;
import net.minecraft.world.border.IBorderListener;
import net.minecraft.world.chunk.listener.IChunkStatusListener;
import net.minecraft.world.chunk.listener.IChunkStatusListenerFactory;
import net.minecraft.world.gen.settings.DimensionGeneratorSettings;
import net.minecraft.world.server.ServerWorld;
import net.minecraft.world.storage.DerivedWorldInfo;
import net.minecraft.world.storage.IServerConfiguration;
import net.minecraft.world.storage.SaveFormat.LevelSave;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.world.WorldEvent;
import net.minecraftforge.fml.common.ObfuscationReflectionHelper;
public class DynamicDimensionHelper
{
// we need to read some private fields in MinecraftServer
// we can use Access Transformers, Accessor Mixins, or ObfuscationReflectionHelper to get at these
// we'll use ORH here as ATs and Mixins seem to be causing headaches for dependant mods lately
// it also lets us define the private-field-getting-shenanigans in the same class we're using them
// it also doesn't need any extra resources or buildscript stuff, which makes this example simpler to describe
public static final Function<MinecraftServer, IChunkStatusListenerFactory> CHUNK_STATUS_LISTENER_FACTORY_FIELD =
getInstanceField(MinecraftServer.class, "field_213220_d");
public static final Function<MinecraftServer, Executor> BACKGROUND_EXECUTOR_FIELD =
getInstanceField(MinecraftServer.class, "field_213217_au");
public static final Function<MinecraftServer, LevelSave> ANVIL_CONVERTER_FOR_ANVIL_FILE_FIELD =
getInstanceField(MinecraftServer.class, "field_71310_m");
// helper for sending a given player to another dimension
// for static dimensions (from datapacks, etc) use MinecraftServer::getWorld to get the world object
// for dynamic dimensions (mystcrafty) use DynamicDimensionHelper.getOrCreateWorld to get the target world
public static void sendPlayerToDimension(ServerPlayerEntity serverPlayer, ServerWorld targetWorld, Vector3d targetVec)
{
// ensure destination chunk is loaded before we put the player in it
targetWorld.getChunk(new BlockPos(targetVec));
serverPlayer.teleport(targetWorld, targetVec.getX(), targetVec.getY(), targetVec.getZ(), serverPlayer.rotationYaw, serverPlayer.rotationPitch);
}
/**
* Gets a world, dynamically creating and registering one if it doesn't exist.<br>
* The dimension registry is stored in the server's level file, all previously registered dimensions are loaded
* and recreated and reregistered whenever the server starts.<br>
* Static, singular dimensions can be registered via this getOrCreateWorld method
* in the FMLServerStartingEvent, which runs immediately after existing dimensions are loaded and registered.<br>
* Dynamic dimensions (mystcraft, etc) seem to be able to be registered at runtime with no repercussions aside from
* lagging the server for a couple seconds while the world initializes.
* @param server a MinecraftServer instance (you can get this from a ServerPlayerEntity or ServerWorld)
* @param worldKey A RegistryKey for your world, you can make one via RegistryKey.getOrCreateKey(Registry.WORLD_KEY, yourWorldResourceLocation);
* @param dimensionFactory A function that produces a new Dimension instance if necessary, given the server and dimension id<br>
* (dimension ID will be the same as the world ID from worldKey)<br>
* It should be assumed that intended dimension has not been created or registered yet,
* so making the factory attempt to get this dimension from the server's dimension registry will fail
* @return Returns a ServerWorld, creating and registering a world and dimension for it if the world does not already exist
*/
public static ServerWorld getOrCreateWorld(MinecraftServer server, RegistryKey<World> worldKey, BiFunction<MinecraftServer, RegistryKey<Dimension>, Dimension> dimensionFactory)
{
// this is marked as deprecated but it's not called from anywhere and I'm not sure how old it is,
// it's probably left over from forge's previous dimension api
// in any case we need to get at the server's world field, and if we didn't use this getter,
// then we'd just end up making a private-field-getter for it ourselves anyway
@SuppressWarnings("deprecation")
Map<RegistryKey<World>, ServerWorld> map = server.forgeGetWorldMap();
// if the world already exists, return it
if (map.containsKey(worldKey))
{
return map.get(worldKey);
}
else
{
// for vanilla worlds, forge fires the world load event *after* the world is put into the map
// we'll do the same for consistency
// (this is why we're not just using map::computeIfAbsent)
ServerWorld newWorld = createAndRegisterWorldAndDimension(server, map, worldKey, dimensionFactory);
return newWorld;
}
}
@SuppressWarnings("deprecation") // markWorldsDirty is deprecated, see below
private static ServerWorld createAndRegisterWorldAndDimension(MinecraftServer server, Map<RegistryKey<World>, ServerWorld> map, RegistryKey<World> worldKey, BiFunction<MinecraftServer, RegistryKey<Dimension>, Dimension> dimensionFactory)
{
ServerWorld overworld = server.getWorld(World.OVERWORLD);
RegistryKey<Dimension> dimensionKey = RegistryKey.getOrCreateKey(Registry.DIMENSION_KEY, worldKey.getLocation());
Dimension dimension = dimensionFactory.apply(server, dimensionKey);
// we need to get some private fields from MinecraftServer here
// chunkStatusListenerFactory
// backgroundExecutor
// anvilConverterForAnvilFile
// the int in create() here is radius of chunks to watch, 11 is what the server uses when it initializes worlds
IChunkStatusListener chunkListener = CHUNK_STATUS_LISTENER_FACTORY_FIELD.apply(server).create(11);
Executor executor = BACKGROUND_EXECUTOR_FIELD.apply(server);
LevelSave levelSave = ANVIL_CONVERTER_FOR_ANVIL_FILE_FIELD.apply(server);
// this is the same order server init creates these worlds:
// instantiate world, add border listener, add to map, fire world load event
// (in server init, the dimension is already in the dimension registry,
// that'll get registered here before the world is instantiated as well)
IServerConfiguration serverConfig = server.getServerConfiguration();
DimensionGeneratorSettings dimensionGeneratorSettings = serverConfig.getDimensionGeneratorSettings();
// this next line registers the Dimension
dimensionGeneratorSettings.func_236224_e_().register(dimensionKey, dimension, Lifecycle.experimental());
DerivedWorldInfo derivedWorldInfo = new DerivedWorldInfo(serverConfig, serverConfig.getServerWorldInfo());
// now we have everything we need to create the world instance
ServerWorld newWorld = new ServerWorld(
server,
executor,
levelSave,
derivedWorldInfo,
worldKey,
dimension.getDimensionType(),
chunkListener,
dimension.getChunkGenerator(),
dimensionGeneratorSettings.func_236227_h_(), // boolean: is-debug-world
BiomeManager.getHashedSeed(dimensionGeneratorSettings.getSeed()),
ImmutableList.of(), // "special spawn list"
// phantoms, raiders, travelling traders, cats are overworld special spawns
// the dimension loader is hardcoded to initialize preexisting non-overworld worlds with no special spawn lists
// so this can probably be left empty for best results and spawns should be handled via other means
false); // "tick time", true for overworld, always false for everything else
// add world border listener
overworld.getWorldBorder().addListener(new IBorderListener.Impl(newWorld.getWorldBorder()));
// register world
map.put(worldKey, newWorld);
// update forge's world cache (very important, if we don't do this then the new world won't tick!)
server.markWorldsDirty();
// fire world load event
MinecraftForge.EVENT_BUS.post(new WorldEvent.Load(newWorld)); // event isn't cancellable
return newWorld;
}
// helper for making the private field getters via reflection
@SuppressWarnings("unchecked") // also throws ClassCastException if the types are wrong
static <FIELDHOLDER,FIELDTYPE> Function<FIELDHOLDER,FIELDTYPE> getInstanceField(Class<FIELDHOLDER> fieldHolderClass, String fieldName)
{
// forge's ORH is needed to reflect into vanilla minecraft java
Field field = ObfuscationReflectionHelper.findField(fieldHolderClass, fieldName);
return instance -> {
try
{
return (FIELDTYPE)(field.get(instance));
}
catch (IllegalArgumentException | IllegalAccessException e)
{
throw new RuntimeException(e);
}
};
}
}
package commoble.hyperbox.dimension;
import java.util.function.Consumer;
import com.mojang.serialization.Codec;
import commoble.hyperbox.Hyperbox;
import commoble.hyperbox.aperture.ApertureBlock;
import net.minecraft.block.BlockState;
import net.minecraft.block.Blocks;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.Direction;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.util.registry.Registry;
import net.minecraft.util.registry.RegistryLookupCodec;
import net.minecraft.world.Blockreader;
import net.minecraft.world.IBlockReader;
import net.minecraft.world.IWorld;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.biome.provider.SingleBiomeProvider;
import net.minecraft.world.chunk.IChunk;
import net.minecraft.world.gen.ChunkGenerator;
import net.minecraft.world.gen.Heightmap.Type;
import net.minecraft.world.gen.WorldGenRegion;
import net.minecraft.world.gen.feature.structure.StructureManager;
import net.minecraft.world.gen.settings.DimensionStructuresSettings;
// if we're creating dimensions at runtime,
// then we'll also need to create chunk generator instances at runtime
// example of how to make a chunk generator for a single-biome dimension, same as the debug world chunk generator
// for more complicated biome schemes, you'll need a proper biome provider in the codec, like NoiseChunkGenerator
public class ExampleChunkGenerator extends ChunkGenerator
{
// we can define the dimension's biome in a json at data/yourmod/worldgen/biome/your_biome
public static RegistryKey<Biome> = RegistryKey.getOrCreateKey(Registry.BIOME_KEY,
new ResourceLocation("yourmodid", "your_biome");
// this Codec will need to be registered to the chunk generator registry in Registry
// during FMLCommonSetupEvent::enqueueWork
// (unless and until a forge registry wrapper becomes made for chunk generators)
public static final Codec<ExampleChunkGenerator> CODEC =
// the registry lookup doesn't actually serialize, so we don't need a field for it
RegistryLookupCodec.getLookUpCodec(Registry.BIOME_KEY)
.xmap(ExampleChunkGenerator::new,ExampleChunkGenerator::getBiomeRegistry)
.codec();
private final Registry<Biome> biomes; public Registry<Biome> getBiomeRegistry() { return this.biomes; }
// create chunk generator at runtime when dynamic dimension is created
public ExampleChunkGenerator(MinecraftServer server)
{
this(server.func_244267_aX() // get dynamic registry
.getRegistry(Registry.BIOME_KEY));
}
// create chunk generator when dimension is loaded from the dimension registry on server init
public ExampleChunkGenerator(Registry<Biome> biomes)
{
super(new SingleBiomeProvider(biomes.getOrThrow(Hyperbox.BIOME_KEY)), new DimensionStructuresSettings(false));
this.biomes = biomes;
}
// get codec
@Override
protected Codec<? extends ChunkGenerator> func_230347_a_()
{
return CODEC;
}
// get chunk generator but with seed
@Override
public ChunkGenerator func_230349_a_(long p_230349_1_)
{
return this;
}
@Override
public void generateSurface(WorldGenRegion worldGenRegion, IChunk chunk)
{
// you can generate stuff in your world here
}
// fill from noise
@Override
public void func_230352_b_(IWorld world, StructureManager structures, IChunk chunk)
{
// you can generate more stuff in your world here
// this is where the flat chunk generator generates flat chunks
}
@Override
public int getHeight(int x, int z, Type heightmapType)
{
// flat chunk generator counts the solid blockstates in its list
// debug chunk generator returns 0
// the "normal" chunk generator generates a height via noise
// we can assume that this is what is used to define the "initial" heightmap
return 0;
}
// get base column
@Override
public IBlockReader func_230348_a_(int x, int z)
{
// flat chunk generator returns a reader over its blockstate list
// debug chunk generator returns a reader over an empty array
// normal chunk generator returns a column whose contents are either default block, default fluid, or air
return new Blockreader(new BlockState[0]);
}
}
// a Dimension is just a DimensionType + a ChunkGenerator
// we can define the dimension type in a json at data/yourmod/worldgen/dimension_type/your_dimension_type.json
// but we'll need to create instances of the chunk generator at runtime since there's no json folder for them
public class ExampleDimensionFactory
{
public static final RegistryKey<DimensionType> TYPE_KEY = RegistryKey.getOrCreateKey(Registry.DIMENSION_TYPE_KEY,
new ResourceLocation("yourmodid", "your_dimension_type"));
public static Dimension createDimension(MinecraftServer server, RegistryKey<Dimension> key)
{
return new Dimension(() -> getDimensionType(server), new ExampleChunkGenerator(server));
}
public static DimensionType getDimensionType(MinecraftServer server)
{
return server.func_244267_aX() // get dynamic registries
.getRegistry(Registry.DIMENSION_TYPE_KEY)
.getOrThrow(TYPE_KEY);
}
}
@TheFloydman
Copy link

Dropping in to say thank you for this! It was a great starting point for dynamic dimensions in 1.19.3. I couldn't wrap my head around the necessary pieces until I came across this example. The hard part now is going to be filling out the chunk generator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment