Jump to content

Modifying day/night cycle sky light levels of the overworld of an existing world


Laike_Endaril

Recommended Posts

Aye, a mouthful.  I'm currently attempting to alter the overworld sky light levels in a way that won't break a users game if they load a world that was created before my mod was installed.

 

I was hoping there would be a base sequence in an array or something that was used with interpolation, so I could simply reflect my way into the data and change it.  Unfortunately this doesn't seem to be the case; as far as I can tell, the sky light level is determined in World.getSunBrightnessFactor:

Spoiler

    /**
     * The current sun brightness factor for this dimension.
     * 0.0f means no light at all, and 1.0f means maximum sunlight.
     * Highly recommended for sunlight detection like solar panel.
     *
     * @return The current brightness factor
     * */
    public float getSunBrightnessFactor(float partialTicks)
    {
        float f = this.getCelestialAngle(partialTicks);
        float f1 = 1.0F - (MathHelper.cos(f * ((float)Math.PI * 2F)) * 2.0F + 0.5F);
        f1 = MathHelper.clamp(f1, 0.0F, 1.0F);
        f1 = 1.0F - f1;
        f1 = (float)((double)f1 * (1.0D - (double)(this.getRainStrength(partialTicks) * 5.0F) / 16.0D));
        f1 = (float)((double)f1 * (1.0D - (double)(this.getThunderStrength(partialTicks) * 5.0F) / 16.0D));
        return f1;
    }

 

This method is in turn ONLY referenced in exactly one place...if no other mods are referencing it, that is...which is in WorldProvider.getSunBrightnessFactor (this method simply returns the result of the first method posted, unless overridden in a subclass).

 

All these being methods, I don't see any way of altering them directly.  I've also considered creating my own class based on one of those 2 classes, or on WorldProviderSurface, but even with that, I don't think there is a way for me to inject my new version where the old one was, because it's only being referenced in an enum called DimensionType:

Spoiler

package net.minecraft.world;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public enum DimensionType
{
    OVERWORLD(0, "overworld", "", WorldProviderSurface.class),
    NETHER(-1, "the_nether", "_nether", WorldProviderHell.class),
    THE_END(1, "the_end", "_end", WorldProviderEnd.class);

    private final int id;
    private final String name;
    private final String suffix;
    private final Class <? extends WorldProvider > clazz;
    private boolean shouldLoadSpawn = false;

    private DimensionType(int idIn, String nameIn, String suffixIn, Class <? extends WorldProvider > clazzIn)
    {
        this.id = idIn;
        this.name = nameIn;
        this.suffix = suffixIn;
        this.clazz = clazzIn;
        this.shouldLoadSpawn = idIn == 0;
    }

    public int getId()
    {
        return this.id;
    }

    public String getName()
    {
        return this.name;
    }

    public String getSuffix()
    {
        return this.suffix;
    }

    public WorldProvider createDimension()
    {
        try
        {
            Constructor <? extends WorldProvider > constructor = this.clazz.getConstructor();
            return constructor.newInstance();
        }
        catch (NoSuchMethodException nosuchmethodexception)
        {
            throw new Error("Could not create new dimension", nosuchmethodexception);
        }
        catch (InvocationTargetException invocationtargetexception)
        {
            throw new Error("Could not create new dimension", invocationtargetexception);
        }
        catch (InstantiationException instantiationexception)
        {
            throw new Error("Could not create new dimension", instantiationexception);
        }
        catch (IllegalAccessException illegalaccessexception)
        {
            throw new Error("Could not create new dimension", illegalaccessexception);
        }
    }

    public static DimensionType getById(int id)
    {
        for (DimensionType dimensiontype : values())
        {
            if (dimensiontype.getId() == id)
            {
                return dimensiontype;
            }
        }

        throw new IllegalArgumentException("Invalid dimension id " + id);
    }

    public boolean shouldLoadSpawn(){ return this.shouldLoadSpawn; }
    public DimensionType setLoadSpawn(boolean value) { this.shouldLoadSpawn = value; return this; }

    private static Class<?>[] ENUM_ARGS = {int.class, String.class, String.class, Class.class};
    static { net.minecraftforge.common.util.EnumHelper.testEnum(DimensionType.class, ENUM_ARGS); }
    public static DimensionType register(String name, String suffix, int id, Class<? extends WorldProvider> provider, boolean keepLoaded)
    {
        String enum_name = name.replace(" ", "_").toLowerCase();
        DimensionType ret = net.minecraftforge.common.util.EnumHelper.addEnum(DimensionType.class, enum_name, ENUM_ARGS,
                id, name, suffix, provider);
        return ret.setLoadSpawn(keepLoaded);
    }
    //TODO: Unregister? There is no way to really delete a enum value...

    public static DimensionType byName(String p_193417_0_)
    {
        for (DimensionType dimensiontype : values())
        {
            if (dimensiontype.getName().equals(p_193417_0_))
            {
                return dimensiontype;
            }
        }

        throw new IllegalArgumentException("Invalid dimension " + p_193417_0_);
    }
}

 

 

And afaik you can't overwrite a previously written enum value, so the register method in there won't do me any good either, right?

 

I continued searching through for a few hours, but haven't come up with a good solution.  At this point I'm thinking I may have made a mistake somewhere overlooked something much simpler and need someone to bash me over the head again.

Link to comment
Share on other sites

17 minutes ago, Laike_Endaril said:

I don't think there is a way for me to inject my new version where the old one was, because it's only being referenced in an enum called DimensionType

17 minutes ago, Laike_Endaril said:

And afaik you can't overwrite a previously written enum value, so the register method in there won't do me any good either, right?

Well, you are completely free to override the values that enum holds though... you know, via reflection.

 

Apart from that while I don't truly understand what you are trying to do I can see that this method

17 minutes ago, Laike_Endaril said:

public float getSunBrightnessFactor(float partialTicks)

Is only called to change the value of World#skylightSubtracted. And if that's a field then you can modify it in a tick event after the world has ticked(hint - use the phase to check that it is run after the rest of vanilla's code). You know, modify using reflection that is.

 

Edited by V0idWa1k3r
Link to comment
Share on other sites

1 minute ago, V0idWa1k3r said:

Well, you are completely free to override the values that enum holds though... you know, via reflection.

I actually attempted this, but had no success.  I'll admit I don't have a lot of experience with enums, though.  I'll re-explore this option.

 

3 minutes ago, V0idWa1k3r said:

while I don't truly understand what you are trying to do

Well, the original goal was to create a companion mod to Grue and Hardcore Darkness which allows Grue to spawn on the surface at night (because Hardcore Darkness is only visual and Grue doesn't have an applicable config option), but at this point my curiosity has me by the ear as well.

 

5 minutes ago, V0idWa1k3r said:

only called to change the value of World#skylightSubtracted. And if that's a field then you can modify it in a tick event after the world has ticked(hint - use the phase to check that it is run after the rest of vanilla's code).

It's being called every tick in WorldServer to calculate the current sky light as well, which was what I was worried about, but now that I think about it, if forge's WorldTick event fires immediately after that and before the data is sent to the client, I should be able to overwrite the field during WorldTick.  I'll probably try this first.  I don't know a lot about the threading on the backend but hopefully I'm safe in assuming that at the very least, WorldServer.tick() and forge's WorldTick event always fire in the same order and at the same frequency?  In any case I'll try it.

Link to comment
Share on other sites

21 minutes ago, Laike_Endaril said:

It's being called every tick in WorldServer to calculate the current sky light as well, which was what I was worried about

That's what I was talking about. Look what the WorldServer actually does:

int j = this.calculateSkylightSubtracted(1.0F);
if (j != this.getSkylightSubtracted())
{
  this.setSkylightSubtracted(j);
}

it checks whether the cached value isn't equal to the actual value and if it isn't it sets the field to the new value. That's all it does. Everything else uses the value stored in that field. If you modify the field you give the modified value to everything that uses it.

There is an issue though - WorldServer#updateBlocks is called after that but before forge's tick event. Which means that any block ticking(not a TileEntity! Just the Block#randomTick method being invoked) still has the old value. I don't know how critical that is. 

Everything else apart from blocks is ticked after the forge's event though. 

 

21 minutes ago, Laike_Endaril said:

I don't know a lot about the threading on the backend but hopefully I'm safe in assuming that at the very least, WorldServer.tick() and forge's WorldTick event always fire in the same order and at the same frequency?

They happen on the same thread. There is very little multithreading in the game so you don't need to worry about that.

 

21 minutes ago, Laike_Endaril said:

before the data is sent to the client

I don't think the client is even aware of this value changing actually. It looks like the only time this value is changed is in WorldServer and I don't see it being synced anywhere. I guess the client doesn't need it at all.

 

21 minutes ago, Laike_Endaril said:

I actually attempted this, but had no success.  I'll admit I don't have a lot of experience with enums, though.

In java enums are just a fancy way to define a collection of objects. So reflection works much the same way as it would with any other object. The only difficulty might be the fact that values in enums are usually marked as final, but there is a dirty hack to get around that. Consider the following example:

Field id = EnumDifficulty.class.getDeclaredField("difficultyId");
id.setAccessible(true);
Field modifiers = Field.class.getDeclaredField("modifiers");
modifiers.setAccessible(true);
modifiers.set(id, id.getModifiers() & ~Modifier.FINAL);
id.setInt(EnumDifficulty.EASY, 10000);
System.out.println(EnumDifficulty.EASY.getDifficultyId());

This outputs 10000, even though the initial value was 1.

Link to comment
Share on other sites

I started writing the reflection for altering World.skylightSubtracted when I realized that the method for writing to the field is already public anyway:

    public void setSkylightSubtracted(int newSkylightSubtracted)
    {
        this.skylightSubtracted = newSkylightSubtracted;
    }

 

So I went ahead and tried that in the WorldTick event like so:

    @SubscribeEvent
    public static void worldTick(TickEvent.WorldTickEvent event)
    {
        event.world.setSkylightSubtracted(15);
    }

 

It had the expected result based on what you said about the timings, ie.

...Monsters can spawn during the day, but...

...Block light levels are unchanged (because the light level is being recalculated before then)

 

It's an interesting result, having zombies and skeletons burst into flame the moment they come into existence.  Luckily, my main interest is simply making nighttime a bit darker so this isn't an issue for me.

 

As usual, this conversation has been very educational!  Thanks again Voidwalker, I think I've got the rest of this mod covered (there's really not much else to it).  And as a bonus, I know to look out for the final modifier on enums from now on.

Link to comment
Share on other sites

I have some unfortunate news.  Despite the solution in my previous post working for vanilla mob spawns (but making them burst into flame immediately if set low during the day), it did not allow the Grue to spawn.

 

After realizing this, I also tried the second approach of overwriting the WorldProvider subclass for the overworld referenced by the DimensionType enum (DimensionType.OVERWORLD normally references the WorldProviderSurface class).  The result was that vanilla mobs can not only spawn, but also no longer burst into flame, and their AI acts as if it is dark as well (spiders attack).  The fog is also darkened, but the blocks themselves are not.  The Grue does not spawn using this method either (but here's the code)

 

Main mod class:

Spoiler

import net.minecraft.world.DimensionType;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.Mod.EventHandler;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.relauncher.ReflectionHelper;
import org.apache.logging.log4j.Logger;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

@Mod(modid = TheDarkestNight.MODID, name = TheDarkestNight.NAME, version = TheDarkestNight.VERSION)
public class TheDarkestNight
{
    public static final String MODID = "thedarkestnight";
    public static final String NAME = "The Darkest Night";
    public static final String VERSION = "1.12.2.001";

    private static Logger logger;

    public TheDarkestNight()
    {
        Field f;
        try {
            f = ReflectionHelper.findField(DimensionType.class, "field_186077_g");
        }
        catch (ReflectionHelper.UnableToFindFieldException e)
        {
            f = ReflectionHelper.findField(DimensionType.class, "clazz");
        }
        f.setAccessible(true);

        Field modifiersField = ReflectionHelper.findField(Field.class, "modifiers");
        modifiersField.setAccessible(true);
        try {
            modifiersField.setInt(f, f.getModifiers() & ~Modifier.FINAL);
            f.set(DimensionType.OVERWORLD, WorldProviderSurfaceEdit.class);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    @EventHandler
    public void preInit(FMLPreInitializationEvent event)
    {
        logger = event.getModLog();
    }
}

 

 

WorldProviderSurfaceEdit, a substitute for WorldProviderSurface:

Spoiler

import net.minecraft.world.DimensionType;
import net.minecraft.world.WorldProvider;

public class WorldProviderSurfaceEdit extends WorldProvider
{
    public DimensionType getDimensionType()
    {
        return DimensionType.OVERWORLD;
    }

    @Override
    public boolean canDropChunk(int x, int z)
    {
        return !this.world.isSpawnChunk(x, z) || !this.world.provider.getDimensionType().shouldLoadSpawn();
    }

    @Override
    public float getSunBrightnessFactor(float par1)
    {
        return -1;
    }
}

 

Grue aside, I expected this method to dim the blocks themselves, because I thought that the blocks' sky light levels were based on the calculation in World.getSunBrightnessFactor, for which I'm currently redirecting every call to my own function (while in the overworld, at least).  This doesn't appear to be the case, which is confusing because in a normal vanilla game, as we all know, the blocks visually darken (and the sky light level lowers, as shown in debug / F3) as the sun sets, and I can't think of a reason to have a second function that calculates the base sky light level.

 

For now, I will continue to search through the backend for where the blocks' sky light level is being determined.

 

 

On 11/7/2018 at 12:31 AM, V0idWa1k3r said:

There is an issue though - WorldServer#updateBlocks is called after that but before forge's tick event.

I wasn't sure why this would be relevant for the new approach (because as I said before I assumed the base light level would only ever be calculated in a single method, which I've overridden), but I'm looking through WorldServer.updateBlocks right now to see if I can find anything.

Edited by Laike_Endaril
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Unfortunately, your content contains terms that we do not allow. Please edit your content to remove the highlighted words below.
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.