diff --git a/src/main/java/amidst/AmidstSettings.java b/src/main/java/amidst/AmidstSettings.java index 76561b579..4084e7b2d 100644 --- a/src/main/java/amidst/AmidstSettings.java +++ b/src/main/java/amidst/AmidstSettings.java @@ -28,6 +28,7 @@ public class AmidstSettings { public final Setting showOceanFeatures; public final Setting showNetherFortresses; public final Setting showEndCities; + public final Setting showEndGateways; public final Setting smoothScrolling; public final Setting fragmentFading; @@ -67,6 +68,7 @@ public AmidstSettings(Preferences preferences) { showOceanFeatures = Setting.createBoolean( preferences, "oceanFeaturesIcons", true); showNetherFortresses = Setting.createBoolean( preferences, "netherFortressIcons", false); showEndCities = Setting.createBoolean( preferences, "endCityIcons", false); + showEndGateways = Setting.createBoolean( preferences, "endGatewayIcons", false); smoothScrolling = Setting.createBoolean( preferences, "mapFlicking", true); fragmentFading = Setting.createBoolean( preferences, "mapFading", true); diff --git a/src/main/java/amidst/fragment/Fragment.java b/src/main/java/amidst/fragment/Fragment.java index c9beb287b..11a4fcdf5 100644 --- a/src/main/java/amidst/fragment/Fragment.java +++ b/src/main/java/amidst/fragment/Fragment.java @@ -15,7 +15,9 @@ import amidst.mojangapi.world.coordinates.Resolution; import amidst.mojangapi.world.icon.WorldIcon; import amidst.mojangapi.world.oracle.BiomeDataOracle; -import amidst.mojangapi.world.oracle.EndIsland; +import amidst.mojangapi.world.oracle.end.EndIslandList; +import amidst.mojangapi.world.oracle.end.LargeEndIsland; +import amidst.mojangapi.world.oracle.end.SmallEndIsland; /** * This class contains nearly no logic but only simple and atomic getters and @@ -96,7 +98,7 @@ public class Fragment { private volatile float alpha; private volatile short[][] biomeData; - private volatile List endIslands; + private volatile EndIslandList endIslands; private final AtomicReferenceArray images; private final AtomicReferenceArray> worldIcons; @@ -135,11 +137,19 @@ public short getBiomeDataAt(int x, int y) { return biomeData[x][y]; } - public void setEndIslands(List endIslands) { + public void setEndIslands(EndIslandList endIslands) { this.endIslands = endIslands; } - public List getEndIslands() { + public List getLargeEndIslands() { + return endIslands.getLargeIslands(); + } + + public List getSmallEndIslands() { + return endIslands.getSmallIslands(); + } + + public EndIslandList getEndIslands() { return endIslands; } diff --git a/src/main/java/amidst/fragment/colorprovider/TheEndColorProvider.java b/src/main/java/amidst/fragment/colorprovider/TheEndColorProvider.java index 31e58be56..59403310f 100644 --- a/src/main/java/amidst/fragment/colorprovider/TheEndColorProvider.java +++ b/src/main/java/amidst/fragment/colorprovider/TheEndColorProvider.java @@ -7,7 +7,9 @@ import amidst.documentation.ThreadSafe; import amidst.fragment.Fragment; import amidst.mojangapi.world.Dimension; -import amidst.mojangapi.world.oracle.EndIsland; +import amidst.mojangapi.world.oracle.end.EndIslandList; +import amidst.mojangapi.world.oracle.end.LargeEndIsland; +import amidst.mojangapi.world.oracle.end.SmallEndIsland; @ThreadSafe public class TheEndColorProvider implements ColorProvider { @@ -31,8 +33,8 @@ public int getColorAt(Dimension dimension, Fragment fragment, long cornerX, long long xAsQuarter = cornerX + x; long yAsQuarter = cornerY + y; return getColorAt( - (int) (xAsQuarter << 2), - (int) (yAsQuarter << 2), + xAsQuarter << 2, + yAsQuarter << 2, xAsQuarter >> 2, yAsQuarter >> 2, (int) (x % TEXTURES_WIDTH), @@ -41,26 +43,27 @@ public int getColorAt(Dimension dimension, Fragment fragment, long cornerX, long } private int getColorAt( - int x, - int y, + long x, + long y, long chunkX, long chunkY, int textureX, int textureY, - List endIslands) { - // Determine whether this + EndIslandList endIslands) { + float maxInfluence = getMaxInfluence(x, y, endIslands); if (maxInfluence >= INFLUENCE_FADE_START) { // Draw endstone island return getEndStoneTextureAt(textureX, textureY); } else { - return getFadingColorAt(chunkX, chunkY, textureX, textureY, maxInfluence); + // Draw fade and small islands + return getOuterColorAt(x, y, chunkX, chunkY, textureX, textureY, maxInfluence, endIslands); } } - private float getMaxInfluence(int x, int y, List endIslands) { + private float getMaxInfluence(long x, long y, EndIslandList endIslands) { float result = -100.0f; - for (EndIsland island : endIslands) { + for (LargeEndIsland island : endIslands.getLargeIslands()) { float influence = island.influenceAtBlock(x, y); if (result < influence) { result = influence; @@ -69,35 +72,68 @@ private float getMaxInfluence(int x, int y, List endIslands) { return result; } - private int getFadingColorAt(long chunkX, long chunkY, int textureX, int textureY, float maxInfluence) { + private int getOuterColorAt( + long x, + long y, + long chunkX, + long chunkY, + int textureX, + int textureY, + float maxInfluence, + EndIslandList endIslands) { int result = VOID_TRANSPARENT_BLACK; - if (showRockyShores(chunkX, chunkY)) { + + // The small islands list is null if the version doesn't support them + if (endIslands.getSmallIslands() != null) { + // Small islands can leak into other biomes if they spawn close enough, so we want to set this to when large islands start fading so they merge smoothly + if(maxInfluence <= INFLUENCE_FADE_START) { + result = getSmallIslandSSAAPixel(x, y, textureX, textureY, endIslands.getSmallIslands()); + } + } else if (showOldRockyShores(chunkX, chunkY)) { result = getRockyShoresTextureAt(textureX, textureY); } + if (maxInfluence > INFLUENCE_FADE_FINISH) { // Fade out the endstone - this is the edge of an island - int pixelAlpha = result >>> 24; - int fadingIslandAlpha = getFadingIslandAlpha(maxInfluence); - if (fadingIslandAlpha > pixelAlpha) { - // favor the island pixel instead of the rocky shores pixel - // (Should look perfect without needing to blend, because - // rocky shore is still endstone texture) - return getFadedEndStoneTextureAt(textureX, textureY, fadingIslandAlpha); - } + int pixelAlpha = (result >>> 24) + getFadingIslandAlpha(maxInfluence); + // Add alphas together to blend + return getFadedEndStoneTextureAt(textureX, textureY, pixelAlpha); } return result; } - - /** - * Determine if the chunk may contain miniature islands. - */ - private boolean showRockyShores(long chunkX, long chunkY) { - return (chunkX * chunkX + chunkY * chunkY) > 4096; + + private static final double ALPHA_INCREMENT = 63.75d; + private static final int[] NEIGHBORING_PIXEL_TABLE = { + 0, 0, + 1, 0, + 0, 1, + 1, 1 + }; + + // Anti-aliased pixel through taking 4 samples and blending them + private int getSmallIslandSSAAPixel(long x, long y, int textureX, int textureY, List smallIslands) { + double alpha = 0; + for(SmallEndIsland smallIsland : smallIslands) { + for(int i = 0; i <= 3; i++) { + if(smallIsland.isOnIsland(x + NEIGHBORING_PIXEL_TABLE[(i * 2)], y + NEIGHBORING_PIXEL_TABLE[(i * 2) + 1])) { + alpha += ALPHA_INCREMENT; + } + } + } + + return alpha == 0 ? VOID_TRANSPARENT_BLACK : getFadedEndStoneTextureAt(textureX, textureY, (int) alpha); } private int getFadingIslandAlpha(float maxInfluence) { return 255 - (int) (255 * (INFLUENCE_FADE_START - maxInfluence) / INFLUENCE_FADE_RANGE); } + + /** + * Determine whether to show the rocky shores texture. + */ + private boolean showOldRockyShores(long chunkX, long chunkY) { + return (chunkX * chunkX + chunkY * chunkY) > 4096L; + } private int getEndStoneTextureAt(int textureX, int textureY) { return TEXTURES.getRGB(textureX, textureY); @@ -108,12 +144,14 @@ private int getEndStoneTextureAt(int textureX, int textureY) { * from the world seed, like chorus plants they are decorations whose PRNG * state depends on the order chunks are created/explored in. This makes me * sad :( Let's use a symbolic texture, since we can't plot them properly. + * + * EDIT: This isn't true past 1.13, they can be generated from the seed. */ private int getRockyShoresTextureAt(int textureX, int textureY) { return TEXTURES.getRGB(textureX, textureY + TEXTURES_HEIGHT); } private int getFadedEndStoneTextureAt(int textureX, int textureY, int alpha) { - return (getEndStoneTextureAt(textureX, textureY) & 0x00FFFFFF) | (alpha << 24); + return (getEndStoneTextureAt(textureX, textureY) & 0x00FFFFFF) | (Math.min(alpha, 0xFF) << 24); } } diff --git a/src/main/java/amidst/fragment/constructor/EndIslandsConstructor.java b/src/main/java/amidst/fragment/constructor/EndIslandsConstructor.java index c5f929a1b..e4cfb28a7 100644 --- a/src/main/java/amidst/fragment/constructor/EndIslandsConstructor.java +++ b/src/main/java/amidst/fragment/constructor/EndIslandsConstructor.java @@ -6,12 +6,13 @@ import amidst.documentation.CalledOnlyBy; import amidst.documentation.Immutable; import amidst.fragment.Fragment; +import amidst.mojangapi.world.oracle.end.EndIslandList; @Immutable public class EndIslandsConstructor implements FragmentConstructor { @CalledOnlyBy(AmidstThread.EDT) @Override public void construct(Fragment fragment) { - fragment.setEndIslands(Collections.emptyList()); + fragment.setEndIslands(new EndIslandList(Collections.emptyList(), Collections.emptyList())); } } diff --git a/src/main/java/amidst/fragment/layer/LayerBuilder.java b/src/main/java/amidst/fragment/layer/LayerBuilder.java index 6fcc79d11..59f88c7a6 100644 --- a/src/main/java/amidst/fragment/layer/LayerBuilder.java +++ b/src/main/java/amidst/fragment/layer/LayerBuilder.java @@ -101,6 +101,7 @@ private List createDeclarations(AmidstSettings settings, List< declare(settings, declarations, enabledLayers, LayerIds.OCEAN_FEATURES, Dimension.OVERWORLD, false, settings.showOceanFeatures); declare(settings, declarations, enabledLayers, LayerIds.NETHER_FEATURES, Dimension.OVERWORLD, false, settings.showNetherFortresses); declare(settings, declarations, enabledLayers, LayerIds.END_CITY, Dimension.END, false, settings.showEndCities); + declare(settings, declarations, enabledLayers, LayerIds.END_GATEWAY, Dimension.END, false, settings.showEndGateways); // @formatter:on return Collections.unmodifiableList(Arrays.asList(declarations)); } @@ -146,7 +147,8 @@ private Iterable createLoaders( new WorldIconLoader<>(declarations.get(LayerIds.WOODLAND_MANSION),world.getWoodlandMansionProducer()), new WorldIconLoader<>(declarations.get(LayerIds.OCEAN_FEATURES), world.getOceanFeaturesProducer()), new WorldIconLoader<>(declarations.get(LayerIds.NETHER_FEATURES), world.getNetherFortressProducer()), - new WorldIconLoader<>(declarations.get(LayerIds.END_CITY), world.getEndCityProducer(), Fragment::getEndIslands) + new WorldIconLoader<>(declarations.get(LayerIds.END_CITY), world.getEndCityProducer(), Fragment::getLargeEndIslands), + new WorldIconLoader<>(declarations.get(LayerIds.END_GATEWAY), world.getEndGatewayProducer(), Fragment::getEndIslands) )); // @formatter:on } @@ -176,7 +178,8 @@ private Iterable createDrawers( new WorldIconDrawer(declarations.get(LayerIds.WOODLAND_MANSION),zoom, worldIconSelection, settings.useHybridScaling), new WorldIconDrawer(declarations.get(LayerIds.OCEAN_FEATURES), zoom, worldIconSelection, settings.useHybridScaling), new WorldIconDrawer(declarations.get(LayerIds.NETHER_FEATURES), zoom, worldIconSelection, settings.useHybridScaling), - new WorldIconDrawer(declarations.get(LayerIds.END_CITY), zoom, worldIconSelection, settings.useHybridScaling) + new WorldIconDrawer(declarations.get(LayerIds.END_CITY), zoom, worldIconSelection, settings.useHybridScaling), + new WorldIconDrawer(declarations.get(LayerIds.END_GATEWAY), zoom, worldIconSelection, settings.useHybridScaling) )); // @formatter:on } diff --git a/src/main/java/amidst/fragment/layer/LayerIds.java b/src/main/java/amidst/fragment/layer/LayerIds.java index e4b40223a..cfe31d32f 100644 --- a/src/main/java/amidst/fragment/layer/LayerIds.java +++ b/src/main/java/amidst/fragment/layer/LayerIds.java @@ -26,6 +26,7 @@ public class LayerIds { public static final int OCEAN_FEATURES = 14; public static final int NETHER_FEATURES = 15; public static final int END_CITY = 16; - public static final int NUMBER_OF_LAYERS = 17; + public static final int END_GATEWAY = 17; + public static final int NUMBER_OF_LAYERS = 18; // @formatter:on } diff --git a/src/main/java/amidst/fragment/loader/EndIslandsLoader.java b/src/main/java/amidst/fragment/loader/EndIslandsLoader.java index 90a2c336a..d1276044c 100644 --- a/src/main/java/amidst/fragment/loader/EndIslandsLoader.java +++ b/src/main/java/amidst/fragment/loader/EndIslandsLoader.java @@ -1,7 +1,5 @@ package amidst.fragment.loader; -import java.util.List; - import amidst.documentation.AmidstThread; import amidst.documentation.CalledByAny; import amidst.documentation.CalledOnlyBy; @@ -10,8 +8,8 @@ import amidst.fragment.layer.LayerDeclaration; import amidst.mojangapi.world.Dimension; import amidst.mojangapi.world.coordinates.CoordinatesInWorld; -import amidst.mojangapi.world.oracle.EndIsland; -import amidst.mojangapi.world.oracle.EndIslandOracle; +import amidst.mojangapi.world.oracle.end.EndIslandList; +import amidst.mojangapi.world.oracle.end.EndIslandOracle; //TODO: use longs? @NotThreadSafe @@ -42,7 +40,7 @@ private void doLoad(Fragment fragment) { } @CalledOnlyBy(AmidstThread.FRAGMENT_LOADER) - private List getEndIslands(CoordinatesInWorld corner) { + private EndIslandList getEndIslands(CoordinatesInWorld corner) { return endIslandOracle.getAt(corner); } } diff --git a/src/main/java/amidst/gui/main/menu/LayersMenu.java b/src/main/java/amidst/gui/main/menu/LayersMenu.java index a697c8cbf..54c2d3a61 100644 --- a/src/main/java/amidst/gui/main/menu/LayersMenu.java +++ b/src/main/java/amidst/gui/main/menu/LayersMenu.java @@ -78,7 +78,7 @@ private void createOverworldAndEndLayers(Dimension dimension) { createOverworldLayers(dimension); menu.addSeparator(); Menus.radio( menu, dimensionSetting, group, Dimension.END, MenuShortcuts.DISPLAY_DIMENSION_END); - endLayer( settings.showEndCities, "End City Icons", getIcon("end_city.png"), MenuShortcuts.SHOW_END_CITIES, dimension, LayerIds.END_CITY); + createEndLayers(dimension); // @formatter:on } @@ -97,6 +97,14 @@ private void createOverworldLayers(Dimension dimension) { overworldLayer(settings.showNetherFortresses, "Nether Features Icons", getIcon("nether_fortress.png"), MenuShortcuts.SHOW_NETHER_FEATURES, dimension, LayerIds.NETHER_FEATURES); // @formatter:on } + + @CalledOnlyBy(AmidstThread.EDT) + private void createEndLayers(Dimension dimension) { + // @formatter:off + endLayer( settings.showEndCities, "End City Icons", getIcon("end_city.png"), MenuShortcuts.SHOW_END_CITIES, dimension, LayerIds.END_CITY); + endLayer( settings.showEndGateways, "End Gateway Icons", getIcon("end_gateway.png"), MenuShortcuts.SHOW_END_GATEWAYS, dimension, LayerIds.END_GATEWAY); + // @formatter:on + } @CalledOnlyBy(AmidstThread.EDT) private void createAllDimensions() { diff --git a/src/main/java/amidst/gui/main/menu/MenuShortcuts.java b/src/main/java/amidst/gui/main/menu/MenuShortcuts.java index e2c0808e9..d22f94f92 100644 --- a/src/main/java/amidst/gui/main/menu/MenuShortcuts.java +++ b/src/main/java/amidst/gui/main/menu/MenuShortcuts.java @@ -48,6 +48,7 @@ public enum MenuShortcuts implements MenuShortcut { // It's okay to duplicate the Overworld layers shortcuts here, because // the End layers will never be active at the same time. SHOW_END_CITIES("menu 1"), + SHOW_END_GATEWAYS("menu 2"), SHOW_GRID("menu G"), SHOW_PLAYERS("menu P"), diff --git a/src/main/java/amidst/gui/main/viewer/PerViewerFacadeInjector.java b/src/main/java/amidst/gui/main/viewer/PerViewerFacadeInjector.java index fb3c53f8f..4b54509b7 100644 --- a/src/main/java/amidst/gui/main/viewer/PerViewerFacadeInjector.java +++ b/src/main/java/amidst/gui/main/viewer/PerViewerFacadeInjector.java @@ -62,7 +62,7 @@ private static List createWidgets( new SeedAndWorldTypeWidget( CornerAnchorPoint.TOP_LEFT, worldOptions.getWorldSeed(), worldOptions.getWorldType()), new SelectedIconWidget( CornerAnchorPoint.TOP_LEFT, worldIconSelection), debugWidget, - new CursorInformationWidget( CornerAnchorPoint.TOP_RIGHT, graph, translator, settings.dimension, world.getBiomeList()), + new CursorInformationWidget( CornerAnchorPoint.TOP_RIGHT, graph, translator, settings.dimension, world.getBiomeList()), biomeToggleWidget, new BiomeExporterProgressWidget(CornerAnchorPoint.BOTTOM_RIGHT, progressEntrySupplier, -20, settings.showDebug, debugWidget, biomeToggleWidget.getWidth()), biomeWidget diff --git a/src/main/java/amidst/gui/main/viewer/widget/CursorInformationWidget.java b/src/main/java/amidst/gui/main/viewer/widget/CursorInformationWidget.java index 2241c52e5..467f2b6e2 100644 --- a/src/main/java/amidst/gui/main/viewer/widget/CursorInformationWidget.java +++ b/src/main/java/amidst/gui/main/viewer/widget/CursorInformationWidget.java @@ -16,6 +16,7 @@ import amidst.mojangapi.world.biome.BiomeList; import amidst.mojangapi.world.coordinates.CoordinatesInWorld; import amidst.mojangapi.world.coordinates.Resolution; +import amidst.mojangapi.world.oracle.end.EndIslandOracle; import amidst.mojangapi.world.versionfeatures.DefaultBiomes; import amidst.settings.Setting; @@ -61,12 +62,26 @@ private String getBiomeNameAt(CoordinatesInWorld coordinates) { if (dimension.equals(Dimension.OVERWORLD)) { return getOverworldBiomeNameAt(coordinates); } else if (dimension.equals(Dimension.END)) { - return biomeList.getByIdOrNull(DefaultBiomes.theEnd).getName(); + return getEndBiomeNameAt(coordinates); } else { AmidstLogger.warn("unsupported dimension"); return UNKNOWN_BIOME_NAME; } } + + public String getEndBiomeNameAt(CoordinatesInWorld coordinates) { + Fragment fragment = graph.getFragmentAt(coordinates); + if (fragment != null && fragment.isLoaded()) { + Biome biome = biomeList.getByIdOrNull(EndIslandOracle.getBiomeAtBlock(coordinates.getX(), coordinates.getY(), graph.getFragmentAt(coordinates).getLargeEndIslands())); + + if (biome != null) { + return biome.getName(); + } else { + return biomeList.getByIdOrNull(DefaultBiomes.theEnd).getName(); + } + } + return UNKNOWN_BIOME_NAME; + } @CalledOnlyBy(AmidstThread.EDT) private String getOverworldBiomeNameAt(CoordinatesInWorld coordinates) { diff --git a/src/main/java/amidst/mojangapi/minecraftinterface/RecognisedVersion.java b/src/main/java/amidst/mojangapi/minecraftinterface/RecognisedVersion.java index eda436827..7170abee0 100644 --- a/src/main/java/amidst/mojangapi/minecraftinterface/RecognisedVersion.java +++ b/src/main/java/amidst/mojangapi/minecraftinterface/RecognisedVersion.java @@ -150,6 +150,7 @@ public enum RecognisedVersion { _1_11 ("1.11", "rroumhkfph[Llw;mt[J[[Jmp"), // matches the launcher version id: 1.11 1.11-pre1 _16w44a ("16w44a", "rqotmgkfpg[Llv;ms[J[[Jmo"), // matches the launcher version id: 16w44a _16w43a ("16w43a", "rpotmgkfpg[Llv;ms[J[[Jmo"), // matches the launcher version id: 16w43a 16w42a 16w41a 16w40a 16w39c + _16w39a ("16w39a", "rnotmgkfpg[Llv;ms[J[[Jmo"), // matches the launcher version id: 16w39a _16w38a ("16w38a", "rlosmfkepf[Llu;mr[J[[Jmn"), // matches the launcher version id: 16w38a _16w36a ("16w36a", "rkosmfkepf[Llu;mr[J[[Jmn"), // matches the launcher version id: 16w36a _16w35a ("16w35a", "rjosmfkepf[Llu;mr[J[[Jmn"), // matches the launcher version id: 16w35a 16w33a 16w32b diff --git a/src/main/java/amidst/mojangapi/world/World.java b/src/main/java/amidst/mojangapi/world/World.java index bdc59f4a8..e744dc450 100644 --- a/src/main/java/amidst/mojangapi/world/World.java +++ b/src/main/java/amidst/mojangapi/world/World.java @@ -10,9 +10,10 @@ import amidst.mojangapi.world.icon.producer.CachedWorldIconProducer; import amidst.mojangapi.world.icon.producer.WorldIconProducer; import amidst.mojangapi.world.oracle.BiomeDataOracle; -import amidst.mojangapi.world.oracle.EndIsland; -import amidst.mojangapi.world.oracle.EndIslandOracle; import amidst.mojangapi.world.oracle.SlimeChunkOracle; +import amidst.mojangapi.world.oracle.end.EndIslandList; +import amidst.mojangapi.world.oracle.end.EndIslandOracle; +import amidst.mojangapi.world.oracle.end.LargeEndIsland; import amidst.mojangapi.world.player.MovablePlayerList; @ThreadSafe @@ -37,7 +38,8 @@ public class World { private final WorldIconProducer woodlandMansionProducer; private final WorldIconProducer oceanFeaturesProducer; private final WorldIconProducer netherFortressProducer; - private final WorldIconProducer> endCityProducer; + private final WorldIconProducer> endCityProducer; + private final WorldIconProducer endGatewayProducer; public World( WorldOptions worldOptions, @@ -59,7 +61,8 @@ public World( WorldIconProducer woodlandMansionProducer, WorldIconProducer oceanFeaturesProducer, WorldIconProducer netherFortressProducer, - WorldIconProducer> endCityProducer) { + WorldIconProducer> endCityProducer, + WorldIconProducer endGatewayProducer) { this.worldOptions = worldOptions; this.movablePlayerList = movablePlayerList; this.recognisedVersion = recognisedVersion; @@ -80,6 +83,7 @@ public World( this.oceanFeaturesProducer = oceanFeaturesProducer; this.netherFortressProducer = netherFortressProducer; this.endCityProducer = endCityProducer; + this.endGatewayProducer = endGatewayProducer; } public WorldOptions getWorldOptions() { @@ -150,9 +154,13 @@ public WorldIconProducer getNetherFortressProducer() { return netherFortressProducer; } - public WorldIconProducer> getEndCityProducer() { + public WorldIconProducer> getEndCityProducer() { return endCityProducer; } + + public WorldIconProducer getEndGatewayProducer() { + return endGatewayProducer; + } public WorldIconProducer getWoodlandMansionProducer() { return woodlandMansionProducer; diff --git a/src/main/java/amidst/mojangapi/world/WorldBuilder.java b/src/main/java/amidst/mojangapi/world/WorldBuilder.java index 54677eb43..a78a71821 100644 --- a/src/main/java/amidst/mojangapi/world/WorldBuilder.java +++ b/src/main/java/amidst/mojangapi/world/WorldBuilder.java @@ -121,6 +121,7 @@ private World create( versionFeatures.get(FeatureKey.NETHER_FORTRESS_PRODUCER), versionFeatures.get(FeatureKey.BASTION_REMNANT_PRODUCER) ), - versionFeatures.get(FeatureKey.END_CITY_PRODUCER)); + versionFeatures.get(FeatureKey.END_CITY_PRODUCER), + versionFeatures.get(FeatureKey.END_GATEWAY_PRODUCER)); } } diff --git a/src/main/java/amidst/mojangapi/world/icon/producer/EmptyProducer.java b/src/main/java/amidst/mojangapi/world/icon/producer/EmptyProducer.java new file mode 100644 index 000000000..4ffc2dd85 --- /dev/null +++ b/src/main/java/amidst/mojangapi/world/icon/producer/EmptyProducer.java @@ -0,0 +1,14 @@ +package amidst.mojangapi.world.icon.producer; + +import java.util.function.Consumer; + +import amidst.mojangapi.world.coordinates.CoordinatesInWorld; +import amidst.mojangapi.world.icon.WorldIcon; + +public class EmptyProducer extends WorldIconProducer { + + @Override + public void produce(CoordinatesInWorld corner, Consumer consumer, T additionalData) { + } + +} diff --git a/src/main/java/amidst/mojangapi/world/icon/producer/EndGatewayProducer.java b/src/main/java/amidst/mojangapi/world/icon/producer/EndGatewayProducer.java new file mode 100644 index 000000000..07e299eaa --- /dev/null +++ b/src/main/java/amidst/mojangapi/world/icon/producer/EndGatewayProducer.java @@ -0,0 +1,335 @@ +package amidst.mojangapi.world.icon.producer; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.function.Consumer; + +import amidst.documentation.ThreadSafe; +import amidst.fragment.Fragment; +import amidst.mojangapi.world.Dimension; +import amidst.mojangapi.world.coordinates.CoordinatesInWorld; +import amidst.mojangapi.world.coordinates.Resolution; +import amidst.mojangapi.world.icon.WorldIcon; +import amidst.mojangapi.world.oracle.end.EndIslandList; +import amidst.mojangapi.world.oracle.end.EndIslandOracle; +import amidst.mojangapi.world.oracle.end.LargeEndIsland; +import amidst.mojangapi.world.oracle.end.SmallEndIsland; +import amidst.util.FastRand; +import static amidst.mojangapi.world.icon.type.DefaultWorldIconTypes.END_GATEWAY; +import static amidst.mojangapi.world.icon.type.DefaultWorldIconTypes.POSSIBLE_END_GATEWAY;; + +@ThreadSafe +public class EndGatewayProducer extends WorldIconProducer { + private static final int END_GATEWAY_CHANCE = 700; + private static final Resolution RESOLUTION = Resolution.CHUNK; + private static final int SIZE = RESOLUTION.getStepsPerFragment(); + + /* + * We add a buffer room of 16 blocks to go into nearby fragments for if it + * moves out of the original one during tryGetValidLocationFromChunk. This + * does slow it down a bit but it allows us to put the icons in the right + * fragments. + */ + private static final int BUFFER_SIZE = (int) RESOLUTION.convertFromWorldToThis(16); + + /** + * The influence has to be atleast 40 for End Highlands biomes to spawn, + * which is where End Gateways are. + */ + private static final float REQUIRED_BIOME_INFLUENCE = 40.0F; + + /** + * Used as a cache only for the spawn gateways. + */ + private final EndSpawnGatewayProducer spawnProducer; + private final long worldSeed; + + private final boolean generateDecoratorGateways; + private final int featureIndex; + private final int generationStage; + + public EndGatewayProducer(long worldSeed, int featureIndex, int generationStage, EndIslandOracle oracle) { + this.spawnProducer = new EndSpawnGatewayProducer(oracle); + this.worldSeed = worldSeed; + this.featureIndex = featureIndex; + this.generationStage = generationStage; + this.generateDecoratorGateways = true; + } + + public EndGatewayProducer(long worldSeed, EndIslandOracle oracle) { + this.spawnProducer = new EndSpawnGatewayProducer(oracle); + this.worldSeed = worldSeed; + this.featureIndex = 0; + this.generationStage = 0; + this.generateDecoratorGateways = false; + } + + @Override + public void produce(CoordinatesInWorld corner, Consumer consumer, EndIslandList endIslands) { + if (generateDecoratorGateways) { + // The buffer only needs to account for positive changes because it's not possible for it to move backwards out of the fragment. + for (int xRelativeToFragment = -BUFFER_SIZE; xRelativeToFragment < SIZE; xRelativeToFragment++) { + for (int yRelativeToFragment = -BUFFER_SIZE; yRelativeToFragment < SIZE; yRelativeToFragment++) { + generateAt(corner, consumer, endIslands, xRelativeToFragment, yRelativeToFragment); + } + } + } + spawnProducer.produce(corner, consumer, null); + } + + private void generateAt( + CoordinatesInWorld corner, + Consumer consumer, + EndIslandList endIslands, + long xRelativeToFragment, + long yRelativeToFragment) { + long x = xRelativeToFragment + corner.getXAs(RESOLUTION); + long y = yRelativeToFragment + corner.getYAs(RESOLUTION); + CoordinatesInWorld possibleCoordinates = tryGetValidLocationFromChunk(x, y, endIslands, corner); + if (possibleCoordinates != null) { + consumer.accept( + new WorldIcon( + possibleCoordinates, + END_GATEWAY.getLabel(), + END_GATEWAY.getImage(), + Dimension.END, + false)); + } + } + + /** + * Made with help from + * KaptainWutax. + */ + public CoordinatesInWorld tryGetValidLocationFromChunk(long chunkX, long chunkY, EndIslandList endIslands, CoordinatesInWorld corner) { + + if((chunkX * chunkX + chunkY * chunkY) > 4096) { + long blockX = chunkX << 4; + long blockY = chunkY << 4; + + FastRand rand = new FastRand(worldSeed); + long a = rand.nextLong() | 1L; + long b = rand.nextLong() | 1L; + long populationSeed = (long)(int) blockX * a + (long)(int) blockY * b ^ worldSeed; // we do the long -> int -> long conversion to replicate what minecraft does. + long decoratorSeed = populationSeed + featureIndex + 10000 * generationStage; + rand.setSeed(decoratorSeed); + + if(rand.nextInt(END_GATEWAY_CHANCE) == 0) { + for(LargeEndIsland largeIsland : endIslands.getLargeIslands()) { + float biomeInfluence = largeIsland.influenceAtChunk(chunkX, chunkY); + if(biomeInfluence >= REQUIRED_BIOME_INFLUENCE) { + long gatewayX = rand.nextInt(16) + blockX; + long gatewayY = rand.nextInt(16) + blockY; + CoordinatesInWorld coordinates = new CoordinatesInWorld(gatewayX, gatewayY); + if(coordinates.isInBoundsOf(corner, Fragment.SIZE)) { + // While this barely ever is false due to the biome influence check, we do it anyway just to make sure + float placementInfluence = largeIsland.influenceAtBlock(gatewayX, gatewayY); + if(placementInfluence > 0.0F) { + return coordinates; + } else { + List smallIslands = endIslands.getSmallIslands(); + if(smallIslands != null) { + // If this check fails, there's a very small chance that it landed on a small island + for(SmallEndIsland smallIsland : smallIslands) { + if(smallIsland.isOnIsland(gatewayX, gatewayY)) { + return coordinates; + } + } + } + } + } + } + } + } + } + + return null; + } + + private static class EndSpawnGatewayProducer extends CachedWorldIconProducer { + private static final int NUMBER_OF_SPAWN_GATEWAYS = 20; + + private final EndIslandOracle oracle; + + public EndSpawnGatewayProducer(EndIslandOracle oracle) { + this.oracle = oracle; + } + + // I'm sorry for writing this. It's slow, ambiguous, and inaccurate. + // It's the best we have though, and I'm done with messing with this + // method. + @Override + protected List doCreateCache() { + List iconList = new ArrayList(); + for(int i = 0; i < NUMBER_OF_SPAWN_GATEWAYS; i++) { + // Generate inner WorldIcon + int x = floor(96.0D * Math.cos(2.0D * (-Math.PI + 0.15707963267948966D * (double) i))); + int y = floor(96.0D * Math.sin(2.0D * (-Math.PI + 0.15707963267948966D * (double) i))); + CoordinatesInWorld possibleCoordinates = new CoordinatesInWorld(x, y); + iconList.add(new WorldIcon( + possibleCoordinates, + POSSIBLE_END_GATEWAY.getLabel(), + POSSIBLE_END_GATEWAY.getImage(), + Dimension.END, + false + ) + ); + + // Generate outer WorldIcon + // Normalize the coordinates the same way MC would + double doubleX = 0; + double doubleY = 0; + double d0 = (double) ((float) (Math.sqrt((double) x * (double) x + (double) y * (double) y))); // There are this many casts in the MC code + if (d0 >= 1.0E-4D) { + doubleX = (double) x / d0; + doubleY = (double) y / d0; + } + // Scale the coordinates + CoordinatesInWorld coordinates = new CoordinatesInWorld((long) (doubleX * 1024.0D), (long) (doubleY * 1024.0D)); + + // Check for closest land along vector + for (int j = 16; !isChunkIslandlessSlow(coordinates) && j-- > 0; coordinates = coordinates.add((long) (doubleX * -16.0D), (long) (doubleY * -16.0D))) { + } + + for (int k = 16; isChunkIslandlessSlow(coordinates) && k-- > 0; coordinates = coordinates.add((long) (doubleX * 16.0D), (long) (doubleY * 16.0D))) { + } + + // Mess with the coords a bit more + coordinates = findSpawnpoint(coordinates, true); + coordinates = findHighestBlock(coordinates, 16); + + iconList.add(new WorldIcon( + coordinates, + POSSIBLE_END_GATEWAY.getLabel(), + POSSIBLE_END_GATEWAY.getImage(), + Dimension.END, + false + ) + ); + } + return iconList; + } + + /* + * This number is supposed to sort of guess whether end + * stone might be at a particular location. + */ + private static final float ISLAND_INFLUENCE_THRESHOLD = -20.0F; + + @SuppressWarnings("unused") + private boolean isChunkIslandlessFast(CoordinatesInWorld blockCoords) { + for(LargeEndIsland island : oracle.getLargeIslandsAt(blockCoords)) { + if(island.influenceAtChunk(blockCoords.getX() >> 4, blockCoords.getY() >> 4) >= ISLAND_INFLUENCE_THRESHOLD) { + return false; + } + } + return true; + } + + private boolean isChunkIslandlessSlow(CoordinatesInWorld blockCoords) { + EndIslandList endIslands = oracle.getAt(blockCoords); + + List smallIslands = endIslands.getSmallIslands(); + if(smallIslands != null) { + // Small Islands + for(SmallEndIsland island : smallIslands) { + for (long x = blockCoords.getX() & -16; x < (blockCoords.getX() | 15); x++) { + for (long y = blockCoords.getY() & -16; y < (blockCoords.getY() | 15); y++) { + if(island.isOnIsland(x, y)) { + return false; + } + } + } + } + } + + // Large Islands + for(LargeEndIsland island : endIslands.getLargeIslands()) { + for (long x = blockCoords.getX() & -16; x < (blockCoords.getX() | 15); x++) { + for (long y = blockCoords.getY() & -16; y < (blockCoords.getY() | 15); y++) { + if(island.influenceAtBlock(x, y) >= ISLAND_INFLUENCE_THRESHOLD) { + return false; + } + } + } + } + return true; + } + + private CoordinatesInWorld findSpawnpoint(CoordinatesInWorld blockCoords, boolean guaranteeEndStone) { + EndIslandList endIslands = oracle.getAt(blockCoords); + for (long y = blockCoords.getY() & -16; y < (blockCoords.getY() | 15); y++) { + for (long x = blockCoords.getX() & -16; x < (blockCoords.getX() | 15); x++) { + // We do the large end islands first because they tend to be not as high + for (LargeEndIsland island : endIslands.getLargeIslands()) { + if (island.influenceAtBlock(x, y) >= (guaranteeEndStone ? 0.0F : ISLAND_INFLUENCE_THRESHOLD)) { + return new CoordinatesInWorld(x, y); + } + } + + List smallIslands = endIslands.getSmallIslands(); + if(smallIslands != null) { + // We want the lowest small end islands first, so we sort them. + smallIslands.sort(((Comparator)(e1, e2) -> Integer.compare(e1.getHeight(), e2.getHeight())).reversed()); + for (SmallEndIsland island : smallIslands) { + if (island.isOnIsland(x, y)) { + return new CoordinatesInWorld(x, y); + } + } + } + } + } + return blockCoords; + } + + private CoordinatesInWorld findHighestBlock(CoordinatesInWorld blockCoords, int radius) { + return findHighestBlock(blockCoords, -radius, -radius, radius, radius); + } + + private CoordinatesInWorld findHighestBlock(CoordinatesInWorld blockCoords, int startX, int startY, int endX, int endY) { + float highestInfluence = -100.0F; + int highestBlock = 0; + long highestX = blockCoords.getX(); + long highestY = blockCoords.getY(); + EndIslandList endIslands = oracle.getAt(blockCoords); + + for (long x = blockCoords.getX() + startX; x <= blockCoords.getX() + endX; ++x) { + for (long y = blockCoords.getY() + startY; y <= blockCoords.getY() + endY; ++y) { + float coordInfluence = EndIslandOracle.getInfluenceAtBlock(x, y, endIslands.getLargeIslands()); + if(coordInfluence > highestInfluence) { + highestInfluence = coordInfluence; + highestX = x; + highestY = y; + } + } + } + + List smallIslands = endIslands.getSmallIslands(); + if(smallIslands != null) { + for (long x = blockCoords.getX() + startX; x <= blockCoords.getX() + endX; ++x) { + for (long y = blockCoords.getY() + startY; y <= blockCoords.getY() + endY; ++y) { + for (SmallEndIsland island : smallIslands) { + if (island.isOnIsland(x, y)) { + int coordHeight = island.getHeight(); + if(coordHeight > highestBlock) { + highestBlock = coordHeight; + highestX = x; + highestY = y; + } + } + } + } + } + } + + return new CoordinatesInWorld(highestX, highestY); + } + + private static int floor(double value) { + int i = (int) value; + return value < (double) i ? i - 1 : i; + } + } + +} diff --git a/src/main/java/amidst/mojangapi/world/icon/type/DefaultWorldIconTypes.java b/src/main/java/amidst/mojangapi/world/icon/type/DefaultWorldIconTypes.java index cffe16760..03ed1c183 100644 --- a/src/main/java/amidst/mojangapi/world/icon/type/DefaultWorldIconTypes.java +++ b/src/main/java/amidst/mojangapi/world/icon/type/DefaultWorldIconTypes.java @@ -31,6 +31,8 @@ public enum DefaultWorldIconTypes { WOODLAND_MANSION ("woodland_mansion", "Woodland Mansion"), END_CITY ("end_city", "Likely End City"), POSSIBLE_END_CITY ("possible_end_city", "Possible End City"), + END_GATEWAY ("end_gateway", "End Gateway"), + POSSIBLE_END_GATEWAY("possible_end_gateway", "Possible End Gateway"), OCEAN_RUINS ("ocean_ruins", "Ocean Ruins"), SHIPWRECK ("shipwreck", "Shipwreck"), BURIED_TREASURE ("buried_treasure", "Buried Treasure"), diff --git a/src/main/java/amidst/mojangapi/world/icon/type/EndCityWorldIconTypeProvider.java b/src/main/java/amidst/mojangapi/world/icon/type/EndCityWorldIconTypeProvider.java index a052dc06f..16e5497b4 100644 --- a/src/main/java/amidst/mojangapi/world/icon/type/EndCityWorldIconTypeProvider.java +++ b/src/main/java/amidst/mojangapi/world/icon/type/EndCityWorldIconTypeProvider.java @@ -2,9 +2,9 @@ import java.util.List; -import amidst.mojangapi.world.oracle.EndIsland; +import amidst.mojangapi.world.oracle.end.LargeEndIsland; -public class EndCityWorldIconTypeProvider implements WorldIconTypeProvider> { +public class EndCityWorldIconTypeProvider implements WorldIconTypeProvider> { /** * REQUIRED_INFLUENCE is a value between 0 and 80 that I'm finding by trial * and error. If the island influence is 0 or higher then an End City can @@ -16,10 +16,9 @@ public class EndCityWorldIconTypeProvider implements WorldIconTypeProvider endIslands) { - // Convert coordinates to long to guard against overflow + public DefaultWorldIconTypes get(int chunkX, int chunkY, List largeEndIslands) { if (((long) chunkX * (long) chunkX + (long) chunkY * (long) chunkY) > 4096) { - return hasSuitableIslandFoundation(chunkX, chunkY, endIslands); + return hasSuitableIslandFoundation(chunkX, chunkY, largeEndIslands); } else { return null; } @@ -36,9 +35,9 @@ public DefaultWorldIconTypes get(int chunkX, int chunkY, List endIsla * * In the meantime, fall back on the requiredInfluence heuristic */ - private DefaultWorldIconTypes hasSuitableIslandFoundation(int chunkX, int chunkY, List endIslands) { + private DefaultWorldIconTypes hasSuitableIslandFoundation(int chunkX, int chunkY, List largeEndIslands) { DefaultWorldIconTypes result = null; - for (EndIsland island : endIslands) { + for (LargeEndIsland island : largeEndIslands) { float influence = island.influenceAtChunk(chunkX, chunkY); if (influence >= REQUIRED_INFLUENCE) { return DefaultWorldIconTypes.END_CITY; diff --git a/src/main/java/amidst/mojangapi/world/oracle/EndIslandOracle.java b/src/main/java/amidst/mojangapi/world/oracle/EndIslandOracle.java deleted file mode 100644 index 8aa51832c..000000000 --- a/src/main/java/amidst/mojangapi/world/oracle/EndIslandOracle.java +++ /dev/null @@ -1,160 +0,0 @@ -package amidst.mojangapi.world.oracle; - -import java.util.LinkedList; -import java.util.List; - -import amidst.documentation.ThreadSafe; -import amidst.mojangapi.world.coordinates.CoordinatesInWorld; -import amidst.mojangapi.world.coordinates.Resolution; -import amidst.util.FastRand; - -@ThreadSafe -public class EndIslandOracle { - public static EndIslandOracle from(long seed) { - return new EndIslandOracle(createNoiseFunction(seed)); - } - - /** - * Returns the noise function using the current seed. - */ - private static SimplexNoise createNoiseFunction(long seed) { - FastRand random = new FastRand(seed); - fakePerlin3dOctavesConstructor(random, 16); - fakePerlin3dOctavesConstructor(random, 16); - fakePerlin3dOctavesConstructor(random, 8); - fakePerlin3dOctavesConstructor(random, 10); - fakePerlin3dOctavesConstructor(random, 16); - return new SimplexNoise(random); - } - - /** - * Mimics the side-effects to the random number generator caused by - * constructing a Perlin3dOctaves instance. - */ - private static void fakePerlin3dOctavesConstructor(FastRand random, int octaveCount) { - for (int i = 0; i < octaveCount; i++) { - fakePerlin3dConstructor(random); - } - } - - /** - * Mimics the side-effects to the random number generator caused by - * constructing a Perlin3d instance. - */ - private static void fakePerlin3dConstructor(FastRand random) { - random.nextDouble(); - random.nextDouble(); - random.nextDouble(); - for (int i = 0; i < 256; i++) { - random.nextInt(256 - i); - } - } - - /** - * Minecraft checks 12 chunks either side of a chunk when assessing island - * influence. - */ - private static final int SURROUNDING_CHUNKS = 12; - - /** - * When cast to double, -0.9 will become -0.8999999761581421, which is why - * you might see that value in Minecraft's .jar - */ - private static final float ISLAND_DENSITY_THRESHOLD = -0.9f; - - /** - * The distance from (0;0) at which islands start to generated - */ - private static final int OUTER_LANDS_DISTANCE_IN_CHUNKS = 64; - - private final SimplexNoise noiseFunction; - - public EndIslandOracle(SimplexNoise noiseFunction) { - this.noiseFunction = noiseFunction; - } - - public List getAt(CoordinatesInWorld corner) { - int steps = Resolution.CHUNK.getStepsPerFragment(); - return findSurroundingIslands( - (int) corner.getXAs(Resolution.CHUNK), - (int) corner.getYAs(Resolution.CHUNK), - steps, - steps); - } - - /** - * Returns a list of all islands that might be touching a chunk-area. - */ - private List findSurroundingIslands( - int chunkX, - int chunkY, - int chunksPerFragmentX, - int chunksPerFragmentY) { - List result = new LinkedList<>(); - for (int y = -SURROUNDING_CHUNKS; y <= chunksPerFragmentY + SURROUNDING_CHUNKS; y++) { - for (int x = -SURROUNDING_CHUNKS; x <= chunksPerFragmentX + SURROUNDING_CHUNKS; x++) { - EndIsland island = tryCreateEndIsland(chunkX + x, chunkY + y); - if (island != null) { - result.add(island); - } - } - } - return result; - } - - /** - * Returns an EndIsland if one has 'grown out' from the chunk, otherwise - * null - */ - private EndIsland tryCreateEndIsland(int chunkX, int chunkY) { - - if (chunkX == 0 && chunkY == 0) { - return createMainEndIsland(chunkX, chunkY); - } else if (!isInRange(chunkX, chunkY, OUTER_LANDS_DISTANCE_IN_CHUNKS)) { - return tryCreateEndIslandInOuterLands(chunkX, chunkY); - } else { - return null; - } - } - - /** - * The main island grows from the origin, with a hard-coded erosion factor - * of 8 - */ - private EndIsland createMainEndIsland(int chunkX, int chunkY) { - return new EndIsland(chunkX, chunkY, 8.0f); - } - - /** - * The chunk is in the outer-islands band - */ - // TODO: check for threading, do we need synchonized or is the SimplexNoise - // class thread safe? - private synchronized EndIsland tryCreateEndIslandInOuterLands(int chunkX, int chunkY) { - if (noiseFunction.noise(chunkX, chunkY) < ISLAND_DENSITY_THRESHOLD) { - return new EndIsland(chunkX, chunkY, getErosionFactor(chunkX, chunkY)); - } else { - return null; - } - } - - /** - * An island (or part of an island) grows out from this chunk, with an - * erosion factor between 9 and 21 (i.e. they will be smaller than the main - * island). - */ - private int getErosionFactor(int chunkX, int chunkY) { - // Convert coordinates to long to guard against overflow - return (int) ((Math.abs((long) chunkX) * 3439 + Math.abs((long) chunkY) * 147) % 13 + 9); - } - - /** - * Is the point (x, y) inside the disk of radius d centered at the origin? - */ - private boolean isInRange(int x, int y, int d) { - // Guard against overflow - if (x < -d || x > d || y < -d || y > d) - return false; - return x * x + y * y <= d * d; - } -} diff --git a/src/main/java/amidst/mojangapi/world/oracle/SimplexNoise.java b/src/main/java/amidst/mojangapi/world/oracle/SimplexNoise.java index 1bcaead06..3b1a5924a 100644 --- a/src/main/java/amidst/mojangapi/world/oracle/SimplexNoise.java +++ b/src/main/java/amidst/mojangapi/world/oracle/SimplexNoise.java @@ -62,11 +62,9 @@ public SimplexNoise(FastRand random) { perm = new short[512]; permMod12 = new short[512]; - // Minecraft uses the PRNG 3 times before building the - // permutation table. - random.nextDouble(); - random.nextDouble(); - random.nextDouble(); + // Minecraft calls nextDouble 3 times before building the permutation + // table, which is equivalent to 6 next calls. + random.advance(6); // Build a permutation table using our seeded PRNG for (short i = 0; i < 256; ++i) { diff --git a/src/main/java/amidst/mojangapi/world/oracle/end/EndIslandList.java b/src/main/java/amidst/mojangapi/world/oracle/end/EndIslandList.java new file mode 100644 index 000000000..6aada026c --- /dev/null +++ b/src/main/java/amidst/mojangapi/world/oracle/end/EndIslandList.java @@ -0,0 +1,25 @@ +package amidst.mojangapi.world.oracle.end; + +import java.util.List; + +import amidst.documentation.Immutable; + +@Immutable +public class EndIslandList { + private final List largeIslands; + private final List smallIslands; + + public EndIslandList(List smallIslands, List largeIslands) { + this.largeIslands = largeIslands; + this.smallIslands = smallIslands; + } + + public List getLargeIslands() { + return largeIslands; + } + + public List getSmallIslands() { + return smallIslands; + } + +} diff --git a/src/main/java/amidst/mojangapi/world/oracle/end/EndIslandOracle.java b/src/main/java/amidst/mojangapi/world/oracle/end/EndIslandOracle.java new file mode 100644 index 000000000..a9a579c90 --- /dev/null +++ b/src/main/java/amidst/mojangapi/world/oracle/end/EndIslandOracle.java @@ -0,0 +1,264 @@ +package amidst.mojangapi.world.oracle.end; + +import java.util.ArrayList; +import java.util.List; + +import amidst.documentation.ThreadSafe; +import amidst.mojangapi.world.coordinates.CoordinatesInWorld; +import amidst.mojangapi.world.coordinates.Resolution; +import amidst.mojangapi.world.oracle.SimplexNoise; +import amidst.mojangapi.world.versionfeatures.DefaultBiomes; +import amidst.util.FastRand; + +@ThreadSafe +public class EndIslandOracle { + public static EndIslandOracle from(long worldSeed, boolean canGenerateSmallIslands) { + return new EndIslandOracle(createNoiseFunction(worldSeed), worldSeed, canGenerateSmallIslands); + } + + /** + * Returns the noise function using the current seed. + */ + private static SimplexNoise createNoiseFunction(long worldSeed) { + FastRand random = new FastRand(worldSeed); + // Mimics the side-effects to the random number generator caused by Minecraft. + // Past 1.13, it just skips the random 17292 times. + random.advance(17292); + return new SimplexNoise(random); + } + + /** + * Minecraft checks 12 chunks either side of a chunk when assessing island + * influence. + */ + private static final int LARGE_ISLAND_SURROUNDING_CHUNKS = 12; + + /** + * add some padding for small islands on fragment borders. + */ + private static final int SMALL_ISLAND_SURROUNDING_CHUNKS = 1; + + /** + * When cast to double, -0.9 will become -0.8999999761581421, which is why + * you might see that value in Minecraft's .jar + */ + private static final float ISLAND_DENSITY_THRESHOLD = -0.9f; + + /** + * The distance from (0;0) at which islands start to generated + */ + private static final int OUTER_LANDS_DISTANCE_IN_CHUNKS = 64; + + private static final long OUTER_LANDS_DISTANCE_IN_BLOCKS = Resolution.CHUNK.convertFromThisToWorld(OUTER_LANDS_DISTANCE_IN_CHUNKS); + + private final SimplexNoise noiseFunction; + private final long worldSeed; + private final boolean canGenerateSmallIslands; + + public EndIslandOracle(SimplexNoise noiseFunction, long worldSeed, boolean canGenerateSmallIslands) { + this.noiseFunction = noiseFunction; + this.worldSeed = worldSeed; + this.canGenerateSmallIslands = canGenerateSmallIslands; + } + + public static int getBiomeAtBlock(long x, long y, List largeIslands) { + if (isInRange(x, y, OUTER_LANDS_DISTANCE_IN_BLOCKS)) { + return DefaultBiomes.theEnd; + } else { + float influence = getInfluenceAtBlock(x, y, largeIslands); + if (influence > 40.0F) { + return DefaultBiomes.theEndHigh; + } else if (influence >= 0.0F) { + return DefaultBiomes.theEndMedium; + } else { + return influence < -20.0F ? DefaultBiomes.theEndLow : DefaultBiomes.theEndBarren; + } + } + } + + public static float getInfluenceAtBlock(long x, long y, List largeIslands) { + float highestInfluence = -100.0f; + + for (LargeEndIsland island : largeIslands) { + if (island instanceof LargeEndIsland) { + float tempInfluence = ((LargeEndIsland) island).influenceAtBlock(x, y); + if (tempInfluence > highestInfluence) { + highestInfluence = tempInfluence; + } + } + } + return highestInfluence; + } + + public EndIslandList getAt(CoordinatesInWorld corner) { + int steps = Resolution.CHUNK.getStepsPerFragment(); + return findSurroundingIslands( + corner.getXAs(Resolution.CHUNK), + corner.getYAs(Resolution.CHUNK), + steps, + steps); + } + + /** + * Returns a list of all islands that might be touching a chunk-area. + */ + private EndIslandList findSurroundingIslands( + long chunkX, + long chunkY, + int chunksPerFragmentX, + int chunksPerFragmentY) { + List largeEndIslands = findSurroundingLargeIslands(chunkX, chunkY, chunksPerFragmentX, chunksPerFragmentY); + List smallEndIslands = findSurroundingSmallIslands(chunkX, chunkY, chunksPerFragmentX, chunksPerFragmentY, largeEndIslands); + return new EndIslandList(smallEndIslands, largeEndIslands); + } + + public List getLargeIslandsAt(CoordinatesInWorld corner) { + int steps = Resolution.CHUNK.getStepsPerFragment(); + return findSurroundingLargeIslands( + corner.getXAs(Resolution.CHUNK), + corner.getYAs(Resolution.CHUNK), + steps, + steps); + } + + /** + * Returns a list of all large islands that might be touching a chunk-area. + */ + private List findSurroundingLargeIslands( + long chunkX, + long chunkY, + int chunksPerFragmentX, + int chunksPerFragmentY) { + List result = new ArrayList<>(); + for (int y = -LARGE_ISLAND_SURROUNDING_CHUNKS; y <= chunksPerFragmentY + LARGE_ISLAND_SURROUNDING_CHUNKS; y++) { + for (int x = -LARGE_ISLAND_SURROUNDING_CHUNKS; x <= chunksPerFragmentX + LARGE_ISLAND_SURROUNDING_CHUNKS; x++) { + LargeEndIsland island = tryCreateLargeEndIsland(chunkX + x, chunkY + y); + if (island != null) { + result.add(island); + } + } + } + return result; + } + + /** + * Returns a LargeEndIsland if one has 'grown out' from the chunk, otherwise + * null + */ + private LargeEndIsland tryCreateLargeEndIsland(long chunkX, long chunkY) { + + if (chunkX == 0 && chunkY == 0) { + return createMainEndIsland(chunkX, chunkY); + } else if (!isInRange(chunkX, chunkY, OUTER_LANDS_DISTANCE_IN_CHUNKS)) { + return tryCreateLargeEndIslandInOuterLands(chunkX, chunkY); + } else { + return null; + } + } + + /** + * The main island grows from the origin, with a hard-coded erosion factor + * of 8 + */ + private LargeEndIsland createMainEndIsland(long chunkX, long chunkY) { + return new LargeEndIsland(chunkX, chunkY, 8.0f); + } + + /** + * The chunk is in the outer-islands band + */ + private LargeEndIsland tryCreateLargeEndIslandInOuterLands(long chunkX, long chunkY) { + if (noiseFunction.noise(chunkX, chunkY) < ISLAND_DENSITY_THRESHOLD) { + return new LargeEndIsland(chunkX, chunkY, getErosionFactor(chunkX, chunkY)); + } else { + return null; + } + } + + /** + * An island (or part of an island) grows out from this chunk, with an + * erosion factor between 9 and 21 (i.e. they will be smaller than the main + * island). + */ + private int getErosionFactor(long chunkX, long chunkY) { + // Convert coordinates to long to guard against overflow + return (int) ((Math.abs(chunkX) * 3439 + Math.abs(chunkY) * 147) % 13 + 9); + } + + private static boolean isInRange(long x, long y, long d) { + return x * x + y * y <= d * d; + } + + private List findSurroundingSmallIslands( + long chunkX, + long chunkY, + int chunksPerFragmentX, + int chunksPerFragmentY, + List largeIslands) { + List result = null; + if(canGenerateSmallIslands) { + result = new ArrayList<>(); + for (int y = -SMALL_ISLAND_SURROUNDING_CHUNKS; y <= chunksPerFragmentY + SMALL_ISLAND_SURROUNDING_CHUNKS; y++) { + for (int x = -SMALL_ISLAND_SURROUNDING_CHUNKS; x <= chunksPerFragmentX + SMALL_ISLAND_SURROUNDING_CHUNKS; x++) { + List smallIslands = getSmallIslandsInChunk(chunkX + x, chunkY + y, largeIslands); + if(smallIslands != null) { + result.addAll(smallIslands); + } + } + } + } + return result; + } + + private static final int SMALL_ISLANDS_FEATURE_INDEX = 0; + private static final int SMALL_ISLANDS_GENERATION_STAGE = 0; + + private List getSmallIslandsInChunk(long chunkX, long chunkY, List largeIslands) { + long blockX = chunkX << 4; + long blockY = chunkY << 4; + if (getBiomeAtBlock(blockX, blockY, largeIslands) == DefaultBiomes.theEndLow) { + FastRand rand = new FastRand(worldSeed); + long a = rand.nextLong() | 1L; + long b = rand.nextLong() | 1L; + long populationSeed = (long)(int) blockX * a + (long)(int) blockY * b ^ worldSeed; // we do the long -> int -> long conversion to replicate what minecraft does. + long decoratorSeed = populationSeed + SMALL_ISLANDS_FEATURE_INDEX + 10000 * SMALL_ISLANDS_GENERATION_STAGE; + rand.setSeed(decoratorSeed); + + List smallIslands = new ArrayList<>(); + + if (rand.nextInt(14) == 0) { + long resultX = blockX + rand.nextInt(16); + int resultH = 55 + rand.nextInt(16); + long rexultY = blockY + rand.nextInt(16); + smallIslands.add(new SmallEndIsland(resultX, rexultY, resultH)); + if (rand.nextInt(4) == 0) { + resultX = blockX + rand.nextInt(16); + resultH = 55 + rand.nextInt(16); + rexultY = blockY + rand.nextInt(16); + smallIslands.add(new SmallEndIsland(resultX, rexultY, resultH)); + } + } + + // mc calculates the sizes of the islands after their locations are generated + for(SmallEndIsland island : smallIslands) { + int size = rand.nextInt(3) + 4; + + island.setSize(size + 1); // the size gets padded with an extra block when generated in mc + + // we have to do this so the random gets set correctly for the next end island + float sizeFloat = (float) size; + + while(sizeFloat > 0.5F) { + sizeFloat = (float) ((double) sizeFloat - ((double) rand.nextInt(2) + 0.5D)); + } + } + + return smallIslands; + } + return null; + } + + public boolean canGenerateSmallIslands() { + return canGenerateSmallIslands; + } +} diff --git a/src/main/java/amidst/mojangapi/world/oracle/EndIsland.java b/src/main/java/amidst/mojangapi/world/oracle/end/LargeEndIsland.java similarity index 75% rename from src/main/java/amidst/mojangapi/world/oracle/EndIsland.java rename to src/main/java/amidst/mojangapi/world/oracle/end/LargeEndIsland.java index 7c11cd14b..8324495ba 100644 --- a/src/main/java/amidst/mojangapi/world/oracle/EndIsland.java +++ b/src/main/java/amidst/mojangapi/world/oracle/end/LargeEndIsland.java @@ -1,96 +1,96 @@ -package amidst.mojangapi.world.oracle; - -import amidst.documentation.Immutable; - -// TODO: check sign of differences ... (this.chunkX - chunkX) vs (chunkX - this.chunkX) -@Immutable -public class EndIsland { - private static final int X_ADJUSTMENT = 1; - private static final int Y_ADJUSTMENT = 1; - - private final int chunkX; - private final int chunkY; - private final float erosionFactor; - - protected EndIsland(int chunkX, int chunkY, float erosionFactor) { - this.chunkX = chunkX; - this.chunkY = chunkY; - this.erosionFactor = erosionFactor; - } - - /** - * Retuns a value between 80 and -100 which indicates this island's - * influence at the block coordindates given. A non-negative value indicates - * there will be solid ground, while a negative value indicates the - * rocky-island-shore, which might be solid ground (but that becomes less - * likely the lower the value). - */ - public float influenceAtBlock(int x, int y) { - // Add 8 blocks to both axis because all the Minecraft calculations are - // done using chunk coordinates and are converted as being the center of - // the chunk whenever translated to block coordinates, whereas Amidst - // treats chunk coords as blockCoordinates >> 4. - // This function also does a floating point divide by 16 instead of - // shifting by 4 in order to maintain sub-chunk accuracy with x & y. - float chunkX = (x + 8) / 16.0f; - float chunkY = (y + 8) / 16.0f; - float adjustedX = (this.chunkX - chunkX) * 2 + X_ADJUSTMENT; - float adjustedY = (this.chunkY - chunkY) * 2 + Y_ADJUSTMENT; - return getResult(adjustedX * adjustedX + adjustedY * adjustedY); - } - - /** - * A version of influenceAt() that more exactly adheres to Minecraft's - * algorithm, for use in testing for End Cities. - */ - public float influenceAtChunk(int chunkX, int chunkY) { - int adjustedX = (chunkX - this.chunkX) * 2 + X_ADJUSTMENT; - int adjustedY = (chunkY - this.chunkY) * 2 + Y_ADJUSTMENT; - return getResult(adjustedX * adjustedX + adjustedY * adjustedY); - } - - private float getResult(double squared) { - float result = 100.0f - (float) Math.sqrt(squared) * erosionFactor; - if (result > 80.0f) { - return 80.0f; - } else if (result < -100.0f) { - return -100.0f; - } else { - return result; - } - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + chunkX; - result = prime * result + chunkY; - result = prime * result + Float.floatToIntBits(erosionFactor); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof EndIsland)) { - return false; - } - EndIsland other = (EndIsland) obj; - if (chunkX != other.chunkX) { - return false; - } - if (chunkY != other.chunkY) { - return false; - } - if (Float.floatToIntBits(erosionFactor) != Float.floatToIntBits(other.erosionFactor)) { - return false; - } - return true; - } -} +package amidst.mojangapi.world.oracle.end; + +import amidst.documentation.Immutable; + +// TODO: check sign of differences ... (this.chunkX - chunkX) vs (chunkX - this.chunkX) +@Immutable +public class LargeEndIsland { + private static final short X_ADJUSTMENT = 1; + private static final short Y_ADJUSTMENT = 1; + + private final long chunkX; + private final long chunkY; + private final float erosionFactor; + + protected LargeEndIsland(long chunkX, long chunkY, float erosionFactor) { + this.chunkX = chunkX; + this.chunkY = chunkY; + this.erosionFactor = erosionFactor; + } + + /** + * Retuns a value between 80 and -100 which indicates this island's + * influence at the block coordindates given. A non-negative value indicates + * there will be solid ground, while a negative value indicates the + * rocky-island-shore, which might be solid ground (but that becomes less + * likely the lower the value). + */ + public float influenceAtBlock(long x, long y) { + // Add 8 blocks to both axis because all the Minecraft calculations are + // done using chunk coordinates and are converted as being the center of + // the chunk whenever translated to block coordinates, whereas Amidst + // treats chunk coords as blockCoordinates >> 4. + // This function also does a floating point divide by 16 instead of + // shifting by 4 in order to maintain sub-chunk accuracy with x & y. + float chunkX = (x + 8) / 16.0f; + float chunkY = (y + 8) / 16.0f; + float adjustedX = (this.chunkX - chunkX) * 2 + X_ADJUSTMENT; + float adjustedY = (this.chunkY - chunkY) * 2 + Y_ADJUSTMENT; + return getResult(adjustedX * adjustedX + adjustedY * adjustedY); + } + + /** + * A version of influenceAt() that more exactly adheres to Minecraft's + * algorithm, for use in testing for End Cities. + */ + public float influenceAtChunk(long chunkX, long chunkY) { + long adjustedX = (chunkX - this.chunkX) * 2 + X_ADJUSTMENT; + long adjustedY = (chunkY - this.chunkY) * 2 + Y_ADJUSTMENT; + return getResult(adjustedX * adjustedX + adjustedY * adjustedY); + } + + private float getResult(double squared) { + float result = 100.0f - (float) Math.sqrt(squared) * erosionFactor; + if (result > 80.0f) { + return 80.0f; + } else if (result < -100.0f) { + return -100.0f; + } else { + return result; + } + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = (int) (prime * result + chunkX); + result = (int) (prime * result + chunkY); + result = prime * result + Float.floatToIntBits(erosionFactor); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof LargeEndIsland)) { + return false; + } + LargeEndIsland other = (LargeEndIsland) obj; + if (chunkX != other.chunkX) { + return false; + } + if (chunkY != other.chunkY) { + return false; + } + if (Float.floatToIntBits(erosionFactor) != Float.floatToIntBits(other.erosionFactor)) { + return false; + } + return true; + } +} diff --git a/src/main/java/amidst/mojangapi/world/oracle/end/SmallEndIsland.java b/src/main/java/amidst/mojangapi/world/oracle/end/SmallEndIsland.java new file mode 100644 index 000000000..2f07e807d --- /dev/null +++ b/src/main/java/amidst/mojangapi/world/oracle/end/SmallEndIsland.java @@ -0,0 +1,80 @@ +package amidst.mojangapi.world.oracle.end; + +import java.awt.Point; +import java.util.Objects; + +public class SmallEndIsland { + private final long blockX; + private final long blockY; + private final int height; + private int size; + + protected SmallEndIsland(long blockX, long blockY, int height, int size) { + this.blockX = blockX; + this.blockY = blockY; + this.height = height; + this.size = size; + } + + protected SmallEndIsland(long blockX, long blockY, int height) { + this.blockX = blockX; + this.blockY = blockY; + this.height = height; + } + + public long getX() { + return blockX; + } + + public long getY() { + return blockY; + } + + public int getHeight() { + return height; + } + + public void setSize(int size) { + this.size = size; + } + + public int getSize() { + return size; + } + + public boolean isOnIsland(long x, long y) { + return Point.distanceSq(x, y, blockX, blockY) <= size * size; + } + + @Override + public int hashCode() { + return Objects.hash(blockX, blockY, height, size); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof SmallEndIsland)) { + return false; + } + SmallEndIsland other = (SmallEndIsland) obj; + if (blockX != other.blockX) { + return false; + } + if (blockY != other.blockY) { + return false; + } + if (height != other.height) { + return false; + } + if (size != other.size) { + return false; + } + return true; + } +} diff --git a/src/main/java/amidst/mojangapi/world/versionfeatures/DefaultVersionFeatures.java b/src/main/java/amidst/mojangapi/world/versionfeatures/DefaultVersionFeatures.java index e2d9a4024..e77ab20fd 100644 --- a/src/main/java/amidst/mojangapi/world/versionfeatures/DefaultVersionFeatures.java +++ b/src/main/java/amidst/mojangapi/world/versionfeatures/DefaultVersionFeatures.java @@ -26,6 +26,7 @@ import amidst.mojangapi.world.icon.producer.BastionRemnantProducer; import amidst.mojangapi.world.icon.producer.CachedWorldIconProducer; import amidst.mojangapi.world.icon.producer.ChunkStructureProducer; +import amidst.mojangapi.world.icon.producer.EndGatewayProducer; import amidst.mojangapi.world.icon.producer.NetherFortressProducer_Original; import amidst.mojangapi.world.icon.producer.NetherFortressProducer_Scattered; import amidst.mojangapi.world.icon.producer.NoopProducer; @@ -43,10 +44,11 @@ import amidst.mojangapi.world.icon.type.EndCityWorldIconTypeProvider; import amidst.mojangapi.world.icon.type.ImmutableWorldIconTypeProvider; import amidst.mojangapi.world.oracle.BiomeDataOracle; -import amidst.mojangapi.world.oracle.EndIsland; -import amidst.mojangapi.world.oracle.EndIslandOracle; import amidst.mojangapi.world.oracle.HeuristicWorldSpawnOracle; import amidst.mojangapi.world.oracle.SlimeChunkOracle; +import amidst.mojangapi.world.oracle.end.EndIslandList; +import amidst.mojangapi.world.oracle.end.EndIslandOracle; +import amidst.mojangapi.world.oracle.end.LargeEndIsland; import amidst.util.FastRand; public enum DefaultVersionFeatures { @@ -100,6 +102,7 @@ public static VersionFeatures.Builder builder(WorldOptions worldOptions, Minecra private static final FeatureKey> NETHER_FORTRESS_FUNCTION = FeatureKey.make(); private static final FeatureKey> BASTION_REMNANT_FUNCTION = FeatureKey.make(); private static final FeatureKey BUGGY_STRUCTURE_COORDINATE_MATH = FeatureKey.make(); + private static final FeatureKey END_CAN_GENERATE_SMALL_ISLANDS = FeatureKey.make(); private static final FeatureKey BIOME_DATA_ORACLE_QUARTER_RES_OVERRIDE = FeatureKey.make(); private static final FeatureKey BIOME_DATA_ORACLE_ACCURATE_LOCATION_COUNT = FeatureKey.make(); private static final FeatureKey BIOME_DATA_ORACLE_MIDDLE_OF_CHUNK_OFFSET = FeatureKey.make(); @@ -167,9 +170,10 @@ public static VersionFeatures.Builder builder(WorldOptions worldOptions, Minecra LayerIds.TEMPLE ).sinceExtend(RecognisedVersion._1_8, LayerIds.OCEAN_MONUMENT - ).sinceExtend(RecognisedVersion._15w31c, + ).sinceExtend(RecognisedVersion._15w31c, // Actually 15w31a LayerIds.END_ISLANDS, - LayerIds.END_CITY + LayerIds.END_CITY, + LayerIds.END_GATEWAY ).sinceExtend(RecognisedVersion._16w43a, LayerIds.WOODLAND_MANSION ).sinceExtend(RecognisedVersion._18w09a, @@ -177,9 +181,16 @@ public static VersionFeatures.Builder builder(WorldOptions worldOptions, Minecra ).construct()) .with(FeatureKey.BIOME_LIST, DefaultBiomes.DEFAULT_BIOMES) + .with(FeatureKey.END_ISLAND_ORACLE, VersionFeature.fixed(features -> - EndIslandOracle.from(getWorldSeed(features)) + EndIslandOracle.from(getWorldSeed(features), features.get(END_CAN_GENERATE_SMALL_ISLANDS)) )) + .with(END_CAN_GENERATE_SMALL_ISLANDS, VersionFeature. builder() + .init( + false + ).since(RecognisedVersion._1_13, // TODO: need confirmation on this version + true + ).construct()) .with(FeatureKey.SLIME_CHUNK_ORACLE, VersionFeature.fixed(features -> new SlimeChunkOracle(getWorldSeed(features)) @@ -270,11 +281,11 @@ public static VersionFeatures.Builder builder(WorldOptions worldOptions, Minecra ) .with(NETHER_BUILDING_SEPARATION, VersionFeature.constant((byte) 4)) - .with(FeatureKey.END_CITY_PRODUCER, VersionFeature.>> builder() + .with(FeatureKey.END_CITY_PRODUCER, VersionFeature.>> builder() .init(new NoopProducer<>()) .since(RecognisedVersion._15w31c, VersionFeature.fixed(features -> - new RegionalStructureProducer>( + new RegionalStructureProducer>( Resolution.CHUNK, 8, null, @@ -291,6 +302,16 @@ public static VersionFeatures.Builder builder(WorldOptions worldOptions, Minecra ) ).construct()) + .with(FeatureKey.END_GATEWAY_PRODUCER, VersionFeature.> builder() + .init(new NoopProducer<>()) + .since(RecognisedVersion._15w31c, // Actually 15w31a + VersionFeature.fixed(features -> new EndGatewayProducer(getWorldSeed(features), features.get(FeatureKey.END_ISLAND_ORACLE))) + ).since(RecognisedVersion._16w39a, + VersionFeature.fixed(features -> new EndGatewayProducer(getWorldSeed(features), 0, 3, features.get(FeatureKey.END_ISLAND_ORACLE))) + ).since(RecognisedVersion._20w06a, // TODO: confirm this version is correct; this changed after 1.15.2 and before 1.16 + VersionFeature.fixed(features -> new EndGatewayProducer(getWorldSeed(features), 13, 4, features.get(FeatureKey.END_ISLAND_ORACLE))) + ).construct()) + .with(MINESHAFT_LOCATION_CHECKER, VersionFeature. builder() .init( // Actually starts at beta 1.8 VersionFeature.fixed(features -> new MineshaftAlgorithm_Original(getWorldSeed(features))) diff --git a/src/main/java/amidst/mojangapi/world/versionfeatures/FeatureKey.java b/src/main/java/amidst/mojangapi/world/versionfeatures/FeatureKey.java index 2be2e9ada..17f56c700 100644 --- a/src/main/java/amidst/mojangapi/world/versionfeatures/FeatureKey.java +++ b/src/main/java/amidst/mojangapi/world/versionfeatures/FeatureKey.java @@ -10,40 +10,42 @@ import amidst.mojangapi.world.icon.producer.VillageProducer; import amidst.mojangapi.world.icon.producer.WorldIconProducer; import amidst.mojangapi.world.oracle.BiomeDataOracle; -import amidst.mojangapi.world.oracle.EndIsland; -import amidst.mojangapi.world.oracle.EndIslandOracle; import amidst.mojangapi.world.oracle.SlimeChunkOracle; import amidst.mojangapi.world.oracle.WorldSpawnOracle; +import amidst.mojangapi.world.oracle.end.EndIslandList; +import amidst.mojangapi.world.oracle.end.EndIslandOracle; +import amidst.mojangapi.world.oracle.end.LargeEndIsland; @Immutable public class FeatureKey { // @formatter:off - public static final FeatureKey WORLD_OPTIONS = make(); - public static final FeatureKey OVERWORLD_BIOME_DATA_ORACLE = make(); - public static final FeatureKey> NETHER_BIOME_DATA_ORACLE = make(); - public static final FeatureKey BIOME_LIST = make(); + public static final FeatureKey WORLD_OPTIONS = make(); + public static final FeatureKey OVERWORLD_BIOME_DATA_ORACLE = make(); + public static final FeatureKey> NETHER_BIOME_DATA_ORACLE = make(); + public static final FeatureKey BIOME_LIST = make(); - public static final FeatureKey> ENABLED_LAYERS = make(); - public static final FeatureKey SLIME_CHUNK_ORACLE = make(); - public static final FeatureKey END_ISLAND_ORACLE = make(); - public static final FeatureKey WORLD_SPAWN_ORACLE = make(); - public static final FeatureKey> NETHER_FORTRESS_PRODUCER = make(); - public static final FeatureKey> BASTION_REMNANT_PRODUCER = make(); - public static final FeatureKey>> END_CITY_PRODUCER = make(); - public static final FeatureKey> MINESHAFT_PRODUCER = make(); - public static final FeatureKey STRONGHOLD_PRODUCER = make(); - public static final FeatureKey VILLAGE_PRODUCER = make(); - public static final FeatureKey> PILLAGER_OUTPOST_PRODUCER = make(); - public static final FeatureKey> DESERT_TEMPLE_PRODUCER = make(); - public static final FeatureKey> IGLOO_PRODUCER = make(); - public static final FeatureKey> JUNGLE_TEMPLE_PRODUCER = make(); - public static final FeatureKey> WITCH_HUT_PRODUCER = make(); - public static final FeatureKey> OCEAN_MONUMENT_PRODUCER = make(); - public static final FeatureKey> WOODLAND_MANSION_PRODUCER = make(); - public static final FeatureKey> OCEAN_RUINS_PRODUCER = make(); - public static final FeatureKey> SHIPWRECK_PRODUCER = make(); - public static final FeatureKey> BURIED_TREASURE_PRODUCER = make(); + public static final FeatureKey> ENABLED_LAYERS = make(); + public static final FeatureKey SLIME_CHUNK_ORACLE = make(); + public static final FeatureKey END_ISLAND_ORACLE = make(); + public static final FeatureKey WORLD_SPAWN_ORACLE = make(); + public static final FeatureKey> NETHER_FORTRESS_PRODUCER = make(); + public static final FeatureKey> BASTION_REMNANT_PRODUCER = make(); + public static final FeatureKey>> END_CITY_PRODUCER = make(); + public static final FeatureKey> END_GATEWAY_PRODUCER = make(); + public static final FeatureKey> MINESHAFT_PRODUCER = make(); + public static final FeatureKey STRONGHOLD_PRODUCER = make(); + public static final FeatureKey VILLAGE_PRODUCER = make(); + public static final FeatureKey> PILLAGER_OUTPOST_PRODUCER = make(); + public static final FeatureKey> DESERT_TEMPLE_PRODUCER = make(); + public static final FeatureKey> IGLOO_PRODUCER = make(); + public static final FeatureKey> JUNGLE_TEMPLE_PRODUCER = make(); + public static final FeatureKey> WITCH_HUT_PRODUCER = make(); + public static final FeatureKey> OCEAN_MONUMENT_PRODUCER = make(); + public static final FeatureKey> WOODLAND_MANSION_PRODUCER = make(); + public static final FeatureKey> OCEAN_RUINS_PRODUCER = make(); + public static final FeatureKey> SHIPWRECK_PRODUCER = make(); + public static final FeatureKey> BURIED_TREASURE_PRODUCER = make(); // @formatter:on diff --git a/src/main/java/amidst/util/FastRand.java b/src/main/java/amidst/util/FastRand.java index 839ef93a0..449810306 100644 --- a/src/main/java/amidst/util/FastRand.java +++ b/src/main/java/amidst/util/FastRand.java @@ -13,7 +13,7 @@ public class FastRand { private static final long MULTIPLIER = 0x5DEECE66DL; private static final long ADDEND = 0xBL; - private static final long MASK = (1L << 48) - 1; + private static final long MODULUS = 1L << 48; private static final double DOUBLE_UNIT = 0x1.0p-53; @@ -24,15 +24,46 @@ public FastRand(long seed) { } private long initialScramble(long seed) { - return (seed ^ MULTIPLIER) & MASK; + return (seed ^ MULTIPLIER) & MODULUS - 1; } public void setSeed(long seed) { this.seed = initialScramble(seed); } + public long mod(long n) { + return n & (MODULUS - 1); + } + public void advance() { - seed = (seed * MULTIPLIER + ADDEND) & MASK; + seed = this.mod(seed * MULTIPLIER + ADDEND); + } + + /** + * Stolen from here + */ + public void advance(int steps) { + long multiplier = 1; + long addend = 0; + + long intermediateMultiplier = MULTIPLIER; + long intermediateAddend = ADDEND; + + for(long k = steps; k != 0; k >>>= 1) { + if((k & 1) != 0) { + multiplier *= intermediateMultiplier; + addend = intermediateMultiplier * addend + intermediateAddend; + } + + intermediateAddend = (intermediateMultiplier + 1) * intermediateAddend; + intermediateMultiplier *= intermediateMultiplier; + } + + multiplier = this.mod(multiplier); + addend = this.mod(addend); + + seed = this.mod(seed * multiplier + addend); } private int next(int bits) { diff --git a/src/main/resources/amidst/gui/main/icon/end_gateway.png b/src/main/resources/amidst/gui/main/icon/end_gateway.png new file mode 100644 index 000000000..57b498c1d Binary files /dev/null and b/src/main/resources/amidst/gui/main/icon/end_gateway.png differ diff --git a/src/main/resources/amidst/gui/main/icon/possible_end_city.png b/src/main/resources/amidst/gui/main/icon/possible_end_city.png index 424e39b6a..cac1a137b 100644 Binary files a/src/main/resources/amidst/gui/main/icon/possible_end_city.png and b/src/main/resources/amidst/gui/main/icon/possible_end_city.png differ diff --git a/src/main/resources/amidst/gui/main/icon/possible_end_gateway.png b/src/main/resources/amidst/gui/main/icon/possible_end_gateway.png new file mode 100644 index 000000000..45da96952 Binary files /dev/null and b/src/main/resources/amidst/gui/main/icon/possible_end_gateway.png differ diff --git a/src/test/java/amidst/mojangapi/world/testworld/DefaultTestWorldDirectoryDeclaration.java b/src/test/java/amidst/mojangapi/world/testworld/DefaultTestWorldDirectoryDeclaration.java index 21ebb53bb..cc58d34a8 100644 --- a/src/test/java/amidst/mojangapi/world/testworld/DefaultTestWorldDirectoryDeclaration.java +++ b/src/test/java/amidst/mojangapi/world/testworld/DefaultTestWorldDirectoryDeclaration.java @@ -10,7 +10,7 @@ import amidst.mojangapi.world.testworld.io.TestWorldEntrySerializer; import amidst.mojangapi.world.testworld.storage.json.BiomeDataJson; import amidst.mojangapi.world.testworld.storage.json.CoordinatesCollectionJson; -import amidst.mojangapi.world.testworld.storage.json.EndIslandsJson; +import amidst.mojangapi.world.testworld.storage.json.LargeEndIslandsJson; import amidst.mojangapi.world.testworld.storage.json.SlimeChunksJson; import amidst.mojangapi.world.testworld.storage.json.WorldMetadataJson; @@ -51,11 +51,11 @@ private TestWorldDirectoryDeclaration createDeclaration() { .deserializer(TestWorldEntrySerializer::readBiomeData) .extractor(biomeDataExtractor()) .skipEqualityCheck() - .entry(TestWorldEntryNames.END_ISLANDS, EndIslandsJson.class) + .entry(TestWorldEntryNames.LARGE_END_ISLANDS, LargeEndIslandsJson.class) .serializer(TestWorldEntrySerializer::writeJson) - .deserializer(TestWorldEntrySerializer::readEndIslands) - .extractor(endIslandExtractor()) - .equalityChecker(EndIslandsJson::equals) + .deserializer(TestWorldEntrySerializer::readLargeEndIslands) + .extractor(largeEndIslandExtractor()) + .equalityChecker(LargeEndIslandsJson::equals) .entry(TestWorldEntryNames.SLIME_CHUNKS, SlimeChunksJson.class) .serializer(TestWorldEntrySerializer::writeJson) .deserializer(TestWorldEntrySerializer::readSlimeChunks) @@ -147,8 +147,8 @@ private Function biomeDataExtractor() { }; } - private Function endIslandExtractor() { - return world -> EndIslandsJson.extract(world.getEndIslandOracle(), END_FRAGMENTS_AROUND_ORIGIN); + private Function largeEndIslandExtractor() { + return world -> LargeEndIslandsJson.extract(world.getEndIslandOracle(), END_FRAGMENTS_AROUND_ORIGIN); } private Function worldIconExtractor( @@ -166,7 +166,7 @@ private Function endCityExtractor(DefaultWorld return world -> CoordinatesCollectionJson.extractWorldIcons( world.getEndCityProducer(), worldIconType.getLabel(), - corner -> world.getEndIslandOracle().getAt(corner), + corner -> world.getEndIslandOracle().getLargeIslandsAt(corner), END_FRAGMENTS_AROUND_ORIGIN, MINIMAL_NUMBER_OF_COORDINATES); } diff --git a/src/test/java/amidst/mojangapi/world/testworld/TestWorldEntryNames.java b/src/test/java/amidst/mojangapi/world/testworld/TestWorldEntryNames.java index 3c23ec71f..06aadc466 100644 --- a/src/test/java/amidst/mojangapi/world/testworld/TestWorldEntryNames.java +++ b/src/test/java/amidst/mojangapi/world/testworld/TestWorldEntryNames.java @@ -7,7 +7,7 @@ public class TestWorldEntryNames { public static final String METADATA = "metadata"; public static final String QUARTER_RESOLUTION_BIOME_DATA = "quarter-resolution-biome-data"; public static final String FULL_RESOLUTION_BIOME_DATA = "full-resolution-biome-data"; - public static final String END_ISLANDS = "end-islands"; + public static final String LARGE_END_ISLANDS = "end-islands"; public static final String SLIME_CHUNKS = "slime-chunks"; public static final String SPAWN = "spawn"; public static final String STRONGHOLDS = "strongholds"; @@ -29,7 +29,7 @@ public class TestWorldEntryNames { TestWorldEntryNames.METADATA, TestWorldEntryNames.QUARTER_RESOLUTION_BIOME_DATA, TestWorldEntryNames.FULL_RESOLUTION_BIOME_DATA, - TestWorldEntryNames.END_ISLANDS, + TestWorldEntryNames.LARGE_END_ISLANDS, TestWorldEntryNames.SLIME_CHUNKS, TestWorldEntryNames.SPAWN, TestWorldEntryNames.STRONGHOLDS, diff --git a/src/test/java/amidst/mojangapi/world/testworld/io/TestWorldEntrySerializer.java b/src/test/java/amidst/mojangapi/world/testworld/io/TestWorldEntrySerializer.java index d7d25235b..97f26515c 100644 --- a/src/test/java/amidst/mojangapi/world/testworld/io/TestWorldEntrySerializer.java +++ b/src/test/java/amidst/mojangapi/world/testworld/io/TestWorldEntrySerializer.java @@ -14,7 +14,7 @@ import amidst.documentation.Immutable; import amidst.mojangapi.world.testworld.storage.json.BiomeDataJson; import amidst.mojangapi.world.testworld.storage.json.CoordinatesCollectionJson; -import amidst.mojangapi.world.testworld.storage.json.EndIslandsJson; +import amidst.mojangapi.world.testworld.storage.json.LargeEndIslandsJson; import amidst.mojangapi.world.testworld.storage.json.SlimeChunksJson; import amidst.mojangapi.world.testworld.storage.json.WorldMetadataJson; @@ -35,8 +35,8 @@ public static BiomeDataJson readBiomeData(InputStream is) { return readJson(is, BiomeDataJson.class); } - public static EndIslandsJson readEndIslands(InputStream is) { - return readJson(is, EndIslandsJson.class); + public static LargeEndIslandsJson readLargeEndIslands(InputStream is) { + return readJson(is, LargeEndIslandsJson.class); } public static SlimeChunksJson readSlimeChunks(InputStream is) { diff --git a/src/test/java/amidst/mojangapi/world/testworld/storage/json/EndIslandsJson.java b/src/test/java/amidst/mojangapi/world/testworld/storage/json/LargeEndIslandsJson.java similarity index 57% rename from src/test/java/amidst/mojangapi/world/testworld/storage/json/EndIslandsJson.java rename to src/test/java/amidst/mojangapi/world/testworld/storage/json/LargeEndIslandsJson.java index 557fcd81e..4a6cd4c30 100644 --- a/src/test/java/amidst/mojangapi/world/testworld/storage/json/EndIslandsJson.java +++ b/src/test/java/amidst/mojangapi/world/testworld/storage/json/LargeEndIslandsJson.java @@ -8,25 +8,25 @@ import amidst.documentation.Immutable; import amidst.mojangapi.mocking.FragmentCornerWalker; import amidst.mojangapi.world.coordinates.CoordinatesInWorld; -import amidst.mojangapi.world.oracle.EndIsland; -import amidst.mojangapi.world.oracle.EndIslandOracle; +import amidst.mojangapi.world.oracle.end.EndIslandOracle; +import amidst.mojangapi.world.oracle.end.LargeEndIsland; @Immutable -public class EndIslandsJson { - public static EndIslandsJson extract(EndIslandOracle oracle, int fragmentsAroundOrigin) { - SortedMap> result = new TreeMap<>(); +public class LargeEndIslandsJson { + public static LargeEndIslandsJson extract(EndIslandOracle oracle, int fragmentsAroundOrigin) { + SortedMap> result = new TreeMap<>(); FragmentCornerWalker.walkFragmentsAroundOrigin(fragmentsAroundOrigin).walk( - corner -> result.put(corner, oracle.getAt(corner))); - return new EndIslandsJson(result); + corner -> result.put(corner, oracle.getLargeIslandsAt(corner))); + return new LargeEndIslandsJson(result); } - private volatile SortedMap> endIslands; + private volatile SortedMap> endIslands; @GsonConstructor - public EndIslandsJson() { + public LargeEndIslandsJson() { } - public EndIslandsJson(SortedMap> endIslands) { + public LargeEndIslandsJson(SortedMap> endIslands) { this.endIslands = endIslands; } @@ -46,10 +46,10 @@ public boolean equals(Object obj) { if (obj == null) { return false; } - if (!(obj instanceof EndIslandsJson)) { + if (!(obj instanceof LargeEndIslandsJson)) { return false; } - EndIslandsJson other = (EndIslandsJson) obj; + LargeEndIslandsJson other = (LargeEndIslandsJson) obj; if (endIslands == null) { if (other.endIslands != null) { return false;