diff --git a/build.gradle b/build.gradle index c98f81a..9b8dd10 100644 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,15 @@ buildscript { repositories { maven { url = 'https://files.minecraftforge.net/maven' } + maven { url = 'https://repo.spongepowered.org/maven' } jcenter() mavenCentral() } dependencies { classpath group: 'net.minecraftforge.gradle', name: 'ForgeGradle', version: '3.+', changing: true + + // https://github.com/SpongePowered/MixinGradle + classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT' } } apply plugin: 'net.minecraftforge.gradle' @@ -13,6 +17,8 @@ apply plugin: 'net.minecraftforge.gradle' apply plugin: 'eclipse' apply plugin: 'maven-publish' +apply plugin: 'org.spongepowered.mixin' + version = "${minecraftVersion}-${modVersion}" group = "io.${groupName}.${modId}" archivesBaseName = project.modId @@ -21,6 +27,12 @@ archivesBaseName = project.modId compileJava.sourceCompatibility = compileJava.targetCompatibility = '1.8' sourceCompatibility = targetCompatibility = compileJava.sourceCompatibility +repositories { + mavenLocal() + maven { url 'https://repo.spongepowered.org/maven' } + maven { url 'https://jitpack.io' } +} + minecraft { // The mappings can be changed at any time, and must be in the following format. // snapshot_YYYYMMDD Snapshot are built nightly. @@ -35,6 +47,7 @@ minecraft { runs { client { workingDirectory project.file('run') + arg "-mixin.config=timelib.mixins.json" // Recommended logging data for a userdev environment property 'forge.logging.markers', 'SCAN,REGISTRIES,REGISTRYDUMP' @@ -75,6 +88,18 @@ dependencies { // https://mvnrepository.com/artifact/org.jetbrains/annotations compile group: 'org.jetbrains', name: 'annotations', version: '19.0.0' + + // https://github.com/SpongePowered/Mixin + compile "org.spongepowered:mixin:0.8.1-SNAPSHOT" + + // For lib development through MavenLocal +// implementation "io.yooksi:cocolib:1.15.2-0.+:dev" + + // https://jitpack.io/#yooksi/cocolib + implementation "com.github.yooksi:cocoLib:${cocoLibVersion}:dev" + + // https://jitpack.io/#yooksi/timelib + runtimeOnly "com.github.yooksi:TimeLib:${timeLibVersion}:dev" } // Get properties into the manifest for reading by the runtime.. diff --git a/gradle.properties b/gradle.properties index 01e1247..a2fe578 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,3 +11,6 @@ minecraftVersion=1.15.2 forgeVersion=31.1.63 mappingMCVersion=1.15.1 mappingBuild=20200501 + +cocoLibVersion=dev-SNAPSHOT +timeLibVersion=dev-SNAPSHOT diff --git a/src/main/java/io/yooksi/daylight/Daylight.java b/src/main/java/io/yooksi/daylight/Daylight.java index caf2cb0..d16f349 100644 --- a/src/main/java/io/yooksi/daylight/Daylight.java +++ b/src/main/java/io/yooksi/daylight/Daylight.java @@ -1,7 +1,13 @@ package io.yooksi.daylight; +import io.yooksi.daylight.config.DaylightConfig; +import io.yooksi.daylight.gui.GuiHandler; +import io.yooksi.daylight.gui.TimeCycle; +import net.minecraft.util.ResourceLocation; import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.ModLoadingContext; import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.config.ModConfig; import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import org.apache.logging.log4j.LogManager; @@ -16,9 +22,15 @@ public Daylight() { // Initialize mod logger DTLogger.init(LogManager.getLogger()); + // Initialize all time cycle types + TimeCycle.initialize(); + // Register the setup method for modloading FMLJavaModLoadingContext.get().getModEventBus().addListener(this::setup); + // Register Mod configuration + ModLoadingContext.get().registerConfig(ModConfig.Type.CLIENT, DaylightConfig.CLIENT_SPEC); + // Register GuiHandler for events MinecraftForge.EVENT_BUS.register(new GuiHandler()); } @@ -28,4 +40,11 @@ private void setup(final FMLCommonSetupEvent event) { // some preinit code DTLogger.info("Daytime pre-initialized"); } + + /** + * @return {@code ResourceLocation} pointing to provided path with {@code MODID} as namespace. + */ + public static ResourceLocation getLocation(String path) { + return new ResourceLocation(MODID, path); + } } diff --git a/src/main/java/io/yooksi/daylight/config/ClientConfig.java b/src/main/java/io/yooksi/daylight/config/ClientConfig.java new file mode 100644 index 0000000..f82db55 --- /dev/null +++ b/src/main/java/io/yooksi/daylight/config/ClientConfig.java @@ -0,0 +1,30 @@ +package io.yooksi.daylight.config; + +import io.yooksi.cocolib.gui.Alignment; +import io.yooksi.daylight.Daylight; +import io.yooksi.daylight.gui.TimeCycle; +import net.minecraftforge.common.ForgeConfigSpec; + +public class ClientConfig { + + public static ForgeConfigSpec.ConfigValue guiAlignment; + public static ForgeConfigSpec.ConfigValue guiOffset; + + public ClientConfig(ForgeConfigSpec.Builder builder) { + + guiAlignment = builder + .comment("Defines GUI alignment on main window screen.\n" + + "Allowed Values: TOP_LEFT, TOP_RIGHT, TOP_CENTER, BOTTOM_LEFT, BOTTOM_RIGHT") + .translation("config." + Daylight.MODID + "guiAlignment") + .define("guiAlignment", TimeCycle.Type.DEFAULT_ALIGNMENT.toString()); + + guiOffset = builder + .comment("Defines GUI offset from edge of main window screen.\n" + + "Format: [ , ] (i.e. [ 5, 5 ])") + .translation("config." + Daylight.MODID + "guiOffset") + .define("guiOffset", TimeCycle.Type.DEFAULT_OFFSET.toString()); + + // Finish building configurations + builder.build(); + } +} \ No newline at end of file diff --git a/src/main/java/io/yooksi/daylight/config/DaylightConfig.java b/src/main/java/io/yooksi/daylight/config/DaylightConfig.java new file mode 100644 index 0000000..0668e11 --- /dev/null +++ b/src/main/java/io/yooksi/daylight/config/DaylightConfig.java @@ -0,0 +1,63 @@ +package io.yooksi.daylight.config; + +import io.yooksi.cocolib.gui.Alignment; +import io.yooksi.daylight.DTLogger; +import io.yooksi.daylight.Daylight; +import io.yooksi.daylight.gui.TimeCycle; +import net.minecraftforge.common.ForgeConfigSpec; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.config.ModConfig; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static io.yooksi.cocolib.gui.PlaneGeometry.*; + +@Mod.EventBusSubscriber(modid = Daylight.MODID, bus = Mod.EventBusSubscriber.Bus.MOD) +public class DaylightConfig { + + public static final ClientConfig CLIENT; + public static final ForgeConfigSpec CLIENT_SPEC; + + static { + final Pair specPair = + new ForgeConfigSpec.Builder().configure(ClientConfig::new); + + CLIENT_SPEC = specPair.getRight(); + CLIENT = specPair.getLeft(); + } + + private static final Pattern OFFSET_PATTERN = + Pattern.compile("\\[\\s*(\\d+)\\s*,\\s*(\\d+)\\s*]"); + + @SubscribeEvent + public static void onModConfigEvent(final ModConfig.ModConfigEvent configEvent) { + + if (configEvent.getConfig().getSpec() == CLIENT_SPEC) + { + // Declare default values in case something goes wrong + Alignment guiAlignment = TimeCycle.Type.DEFAULT_ALIGNMENT; + Dimensions guiOffset = TimeCycle.Type.DEFAULT_OFFSET; + try + { + String alignment = ClientConfig.guiAlignment.get(); + guiAlignment = Enum.valueOf(Alignment.class, alignment); + } + catch (IllegalArgumentException | NullPointerException e) { + DTLogger.error("Malformed config value 'guiAlignment'", e); + } + Matcher match = OFFSET_PATTERN.matcher(ClientConfig.guiOffset.get()); + if (match.find()) + { + guiOffset = new Dimensions( + Integer.parseInt(match.group(1)), + Integer.parseInt(match.group(2)) + ); + } + else DTLogger.error("Malformed config value 'guiOffset'"); + TimeCycle.updatePosition(guiAlignment, guiOffset); + } + } +} diff --git a/src/main/java/io/yooksi/daylight/gui/GuiHandler.java b/src/main/java/io/yooksi/daylight/gui/GuiHandler.java new file mode 100644 index 0000000..b0c47ce --- /dev/null +++ b/src/main/java/io/yooksi/daylight/gui/GuiHandler.java @@ -0,0 +1,36 @@ +package io.yooksi.daylight.gui; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.entity.player.ClientPlayerEntity; +import net.minecraft.world.World; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.biome.Biomes; +import net.minecraftforge.client.event.RenderGameOverlayEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; + +@Mod.EventBusSubscriber +public class GuiHandler { + + @SubscribeEvent + public void onPreRenderOverlay(RenderGameOverlayEvent.Pre event) { + + @Nullable World world = Minecraft.getInstance().world; + @Nullable ClientPlayerEntity player = Minecraft.getInstance().player; + + if (world != null && player != null) + { + Biome biome = world.getBiome(player.getPosition()); + TimeCycle cycle = TimeCycle.getForBiome(biome); + + if (cycle != null) { + cycle.updateAndDraw(world); + } + // Default to Plains biome for everything else + else Objects.requireNonNull(TimeCycle.getForBiome(Biomes.PLAINS)).updateAndDraw(world); + } + } + } diff --git a/src/main/java/io/yooksi/daylight/gui/TimeCycle.java b/src/main/java/io/yooksi/daylight/gui/TimeCycle.java new file mode 100644 index 0000000..07f7fee --- /dev/null +++ b/src/main/java/io/yooksi/daylight/gui/TimeCycle.java @@ -0,0 +1,216 @@ +package io.yooksi.daylight.gui; + +import com.google.common.collect.ImmutableList; +import io.yooksi.cocolib.gui.Alignment; +import io.yooksi.cocolib.gui.GuiElement; +import io.yooksi.cocolib.gui.SpriteObject; +import io.yooksi.cocolib.util.DayTime; +import io.yooksi.cocolib.util.RLHelper; +import io.yooksi.daylight.DTLogger; +import io.yooksi.daylight.Daylight; +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.World; +import net.minecraft.world.biome.Biome; +import net.minecraft.world.biome.Biomes; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +import static io.yooksi.cocolib.gui.PlaneGeometry.*; + +/** + * This class controls the visual representation of the passage of time in Minecraft, + * through a {@code GuiElement} that changes in a slideshow manner to inform the + * player what time of day it is with different textures for different biomes. + */ +public class TimeCycle extends SpriteObject { + + public enum Type { + + PLAIN("gui/time_cycle_plains.png", new Biome[] { + Biomes.PLAINS, Biomes.SUNFLOWER_PLAINS + }), + DESERT("gui/time_cycle_desert.png", new Biome[] { + Biomes.DESERT, Biomes.DESERT_HILLS, Biomes.DESERT_LAKES + }), + SNOW("gui/time_cycle_snow.png", new Biome[] { + Biomes.SNOWY_BEACH, Biomes.SNOWY_MOUNTAINS, Biomes.SNOWY_TAIGA, + Biomes.SNOWY_TAIGA_HILLS, Biomes.SNOWY_TAIGA_MOUNTAINS, + Biomes.SNOWY_TUNDRA, Biomes.FROZEN_RIVER + }), + OCEAN("gui/time_cycle_ocean.png", new Biome[] { + Biomes.OCEAN, Biomes.FROZEN_OCEAN, Biomes.COLD_OCEAN, + Biomes.DEEP_COLD_OCEAN, Biomes.DEEP_FROZEN_OCEAN, + Biomes.LUKEWARM_OCEAN, Biomes.DEEP_LUKEWARM_OCEAN, + Biomes.WARM_OCEAN, Biomes.DEEP_OCEAN, Biomes.DEEP_WARM_OCEAN + }); + /* + * These static fields contain data for creating TimeCycle instances + */ + public static final Alignment DEFAULT_ALIGNMENT = Alignment.TOP_RIGHT; + public static final Dimensions DEFAULT_OFFSET = new Dimensions(5, 5); + public static final Dimensions DEFAULT_SIZE = new Dimensions(90, 32); + + private static final Type[] VALUES = Type.values(); + + private final ResourceLocation location; + private final ImmutableList allowedBiomes; + + Type(String texturePath, Biome[] allowedBiomes) { + + location = RLHelper.getTextureLocation(Daylight.MODID, texturePath); + this.allowedBiomes = ImmutableList.copyOf(allowedBiomes); + } + + /** + * @return {@code ResourceLocation} associated with this type. + */ + public ResourceLocation getTextureLocation() { + return location; + } + } + private static final SpriteObject FRAME = SpriteObject.Builder.create( + RLHelper.getTextureLocation(Daylight.MODID, "gui/time_cycle_frame.png")) + .withPos(Alignment.TOP_RIGHT, Type.DEFAULT_OFFSET.getWidth() - 1, + Type.DEFAULT_OFFSET.getHeight() - 1).withSize(91, 33).build(); + + /** Contains all {@code TimeCycle} instances mapped to types. */ + private static Map types; + + /** + * Immutable list of pre-defined allowed biomes. Attempting to change this + * list in any way will result in {@code UnsupportedOperationException}. + */ + private final ImmutableList biomes; + + /** Last recorded {@code dayTime} from advancing time. */ + private long lastDayTime; + + /** + * @param allowedBiomes array of allowed biomes for this time cycle. + * @param builder data to build sprite from. + * + * @throws NullPointerException if any biome {@code element} is null + */ + private TimeCycle(Biome[] allowedBiomes, SpriteObject.Builder builder) { + + super(builder); + biomes = ImmutableList.copyOf(allowedBiomes); + } + + /** + * Initialize all {@code TimeCycle} types in an internal map.
+ * Call this only once from mod initialization phase. + */ + public static void initialize() { + + if (types != null) + { + DTLogger.warn("Wanted to re-initialize time cycles, skipping..."); + return; + } + Map cycles = new java.util.HashMap<>(); + for (Type type : Type.VALUES) { + cycles.put(type, new TimeCycle(type.allowedBiomes.toArray(new Biome[]{}), + SpriteObject.Builder.create(type.location).withPos(Type.DEFAULT_ALIGNMENT, + Type.DEFAULT_OFFSET).withSize(Type.DEFAULT_SIZE))); + } + types = java.util.Collections.unmodifiableMap(new java.util.HashMap<>(cycles)); + } + + /** + * @return first registered {@code TimeCycle} instance that has the given biome + * listed as an allowed biome with the given or {@code null} if none found. + */ + public static @Nullable TimeCycle getForBiome(Biome biome) { + + for (Type type : Type.VALUES) + { + for (Biome allowedBiome : type.allowedBiomes) + { + if (allowedBiome == biome) + return types.get(type); + } + } + return null; + } + + /** + * Recalculate time cycle UV mapping coordinates in the given world. + * The calculation mainly depends on the current time in the + * world and will move the cycle in a seamless loop. + */ + void advanceTime(World world) { + + final long time = DayTime.getTimeOfDay(world); + final int spriteWidth = getWidth(); + final TimeSegment startSegment = TimeSegment.get(time); + + // Calculate how much pixels UV has to be moved along X axis + final int pixels = (int) Math.floor((float)( + startSegment.getElapsedTime(time) / startSegment.tpp) + ); + // Move UV for set pixel count and center by compensating for sprite width + int u = startSegment.uv.x + pixels - spriteWidth / 2; + int v = startSegment.uv.y; + + int uvEdge = u + spriteWidth; + int rowLength = startSegment.getRowLength(); + + // If UV mapping will fall outside texture bound move to next row + if (uvEdge >= rowLength) + { + u = uvEdge - rowLength; + v = startSegment.getNextRow(); + } + getUV().update(u, v); + } + + /** + * Update {@code TimeCycle} sprite alignment and offset with given arguments. + * + * @param alignment relative position on game screen. + * @param offset offset from position relative to alignment. + */ + public static void updatePosition(Alignment alignment, Dimensions offset) { + + for (Map.Entry entry : types.entrySet()) + { + TimeCycle cycle = entry.getValue(); + cycle.align(alignment, offset.getWidth(), offset.getHeight()); + cycle.updateScaledPosition(true); + } + // Don't forget to update the frame as well + FRAME.align(alignment, offset.getWidth() - 1, offset.getHeight() - 1); + FRAME.updateScaledPosition(true); + } + + /** + * Update time cycle sprite UV mapping coordinates based on + * time in the given world and draw HUD on game screen. + */ + void updateAndDraw(World world) { + + final long currentDayTime = world.getDayTime(); + /* + * Recalculate sprite UV mapping coordinates. + * To be performance efficient, update only if + * world day time has changed since last update. + */ + if (lastDayTime != currentDayTime) + { + advanceTime(world); + lastDayTime = currentDayTime; + } + // Draw time cycle on game screen + GuiElement.bindAndDrawTexture(this); + GuiElement.bindAndDrawTexture(FRAME); + } + + /** + * @return {@code true} if the given biome is allowed for this {@code TimeCycle}. + */ + public boolean isAllowedBiome(Biome biome) { + return biomes.contains(biome); + } +} diff --git a/src/main/java/io/yooksi/daylight/gui/TimeSegment.java b/src/main/java/io/yooksi/daylight/gui/TimeSegment.java new file mode 100644 index 0000000..0a6dde8 --- /dev/null +++ b/src/main/java/io/yooksi/daylight/gui/TimeSegment.java @@ -0,0 +1,122 @@ +package io.yooksi.daylight.gui; + +import io.yooksi.cocolib.gui.PlaneGeometry; +import io.yooksi.cocolib.lang.MathTools; +import io.yooksi.cocolib.util.DayTime; + +/** + * Time segments represent texture UV blocks that correspond to different + * times of day. The values here reflect {@link DayTime.Segment} values and + * contain data on how to render the time cycle depending on game time. + */ +public enum TimeSegment { + + DAWN(152, 2), + NOON(121, 0), + DUSK(76, 1), + MIDNIGHT(197, 1); + + /** Length of each texture segment in pixels. */ + public static final double LENGTH = 121D; + + /** + * Maximum amount of texture rows to expect. 0 represents the first row, + * and once the end of the final row is reached rendering will switch + * to row 0 to maintain a seamless animation loop. + */ + public static final int MAX_ROWS = 2; + + /** + * Height of each individual texture row. + * Used to calculate UV mapping coordinate on {@code y} axis. + */ + public static final int ROW_HEIGHT = 32; + + /** + * Length of each individual texture row. + * Used to calculate UV mapping coordinate on {@code x} axis. + * Once the sprite UV reaches the edge of the row it will switch to next row. + */ + public static final int ROW_LENGTH = 256; + + /** + * Length of the last texture row. + * Used in place of {@link #ROW_LENGTH} when on last row. + */ + public static final int LAST_ROW_LENGTH = 242; + + private static final TimeSegment[] VALUES = values(); + + /** UV mapping coordinates marking the start the segment. */ + final PlaneGeometry.Coordinates uv; + + /** Texture row the segment start is located on. */ + final int row; + + /** + * Amount of game ticks represented by the length of + * each pixel along {@code x} axis in each texture row. + */ + final int tpp; + + TimeSegment(int u, int row) { + + this.row = MathTools.getValueInRange(row, 0, MAX_ROWS); + uv = new PlaneGeometry.Coordinates(u, ROW_HEIGHT * this.row); + /* + * Round value to nearest integer to get a smooth transition. + * If we are early it will look like we're traveling back through + * time when then frame repositions to fit the DayTime.Segment + */ + this.tpp = Math.round((float)(getTimeDuration() / LENGTH)); + } + + public int getRowLength() { + return row == TimeSegment.MAX_ROWS ? LAST_ROW_LENGTH : ROW_LENGTH; + } + + public int getNextRow() { + return row == 2 ? 0 : uv.y + ROW_HEIGHT; + } + + /** + * @return {@code DayTime.Segment} associated with this segment. + */ + public DayTime.Segment getDayTimeSegment() { + return DayTime.getSegments()[ordinal()]; + } + + /** + * @return duration of the associated day segment measured in game ticks. + * @see DayTime.Segment#getDuration() + */ + public long getTimeDuration() { + return getDayTimeSegment().getDuration(); + } + + /** + * @param currentTime current day time in the world. + * @return amount of time elapsed from the defined start of the + * associated day segment to the given time. + * + * @see DayTime.Segment#getElapsedTime(long) + */ + public long getElapsedTime(long currentTime) { + return getDayTimeSegment().getElapsedTime(currentTime); + } + + /** + * @param time game time expressed in ticks. + * @return {@code TimeCycle.Segment} associated with the closest + * {@code DayTime.Segment} that matches the given time. + * + * @see DayTime.Segment#get(long) + */ + public static TimeSegment get(long time) { + return VALUES[DayTime.Segment.get(time).ordinal()]; + } + + public int getTicksPerPixel() { + return tpp; + } +} diff --git a/src/main/java/io/yooksi/daylight/util/RLHelper.java b/src/main/java/io/yooksi/daylight/util/RLHelper.java deleted file mode 100644 index 62e62ba..0000000 --- a/src/main/java/io/yooksi/daylight/util/RLHelper.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.yooksi.daylight.util; - -import io.yooksi.daylight.Daylight; -import net.minecraft.util.ResourceLocation; - -/** - * Tiny utility class to help find the right {@code ResourceLocation}. - */ -public final class RLHelper { - - private RLHelper() { - throw new UnsupportedOperationException(); - } - - /** - * @return {@code ResourceLocation} pointing to provided path with {@code MODID} as namespace. - */ - public static ResourceLocation getModResourceLocation(String path) { - return new ResourceLocation(Daylight.MODID, path); - } - - /** - * @return {@code ResourceLocation} pointing to provided path with - * {@code minecraft} as namespace. Registering blocks and items with - * this location will create a registry override (replace). - */ - public static ResourceLocation getOverrideResourceLocation(String path) { - return new ResourceLocation("minecraft", path); - } - - /** - * @return {@code ResourceLocation} pointing to provided path with namespace depending - * on whether we want an override resource location ({@code minecraft}) or not ({@code MODID}). - * - * @see #getOverrideResourceLocation(String) - * @see #getModResourceLocation(String) - */ - public static ResourceLocation getResourceLocation(String path, boolean override) { - return override ? getOverrideResourceLocation(path) : getModResourceLocation(path); - } -} diff --git a/src/main/resources/assets/daylight/textures/gui/time_cycle_desert.png b/src/main/resources/assets/daylight/textures/gui/time_cycle_desert.png new file mode 100644 index 0000000..60c0338 Binary files /dev/null and b/src/main/resources/assets/daylight/textures/gui/time_cycle_desert.png differ diff --git a/src/main/resources/assets/daylight/textures/gui/time_cycle_frame.png b/src/main/resources/assets/daylight/textures/gui/time_cycle_frame.png new file mode 100644 index 0000000..a5b158a Binary files /dev/null and b/src/main/resources/assets/daylight/textures/gui/time_cycle_frame.png differ diff --git a/src/main/resources/assets/daylight/textures/gui/time_cycle_ocean.png b/src/main/resources/assets/daylight/textures/gui/time_cycle_ocean.png new file mode 100644 index 0000000..3c9e133 Binary files /dev/null and b/src/main/resources/assets/daylight/textures/gui/time_cycle_ocean.png differ diff --git a/src/main/resources/assets/daylight/textures/gui/time_cycle_plains.png b/src/main/resources/assets/daylight/textures/gui/time_cycle_plains.png new file mode 100644 index 0000000..f8832bf Binary files /dev/null and b/src/main/resources/assets/daylight/textures/gui/time_cycle_plains.png differ diff --git a/src/main/resources/assets/daylight/textures/gui/time_cycle_snow.png b/src/main/resources/assets/daylight/textures/gui/time_cycle_snow.png new file mode 100644 index 0000000..19fa381 Binary files /dev/null and b/src/main/resources/assets/daylight/textures/gui/time_cycle_snow.png differ