Jump to content

Recommended Posts

Posted (edited)

I wrote a class that stores the BlockPos and BlockState of a specific block that needs to be changed into another block after a certain amount of time. So far I just coded stone bricks being changed into cracked stone bricks.

In every worldtickevent I call a method called onTick in which there is a slight chance (1 in 1000) that this change will occur. The class that does this is called DecayHandler.java

 

DecayHandler.Java :
 

public class DecayHandler {

    public BlockState blockState;
    public BlockPos blockPos;
    public World world;
    private Random random;
    public int decayChance = 1000;
    private boolean decayed = false;

    public DecayHandler(BlockState blockState, BlockPos blockPos, World world){
        this.blockState = blockState;
        this.blockPos = blockPos;
        this.world = world;
        random = new Random();
    }

    public DecayHandler OnTick(){
        //TODO: Refactor in other methods and implement BlockSwapper
        if(random.nextInt(decayChance) == 0 && !decayed){
            System.out.println(this.blockPos + " Decayed");
            if(world.setBlockAndUpdate(blockPos, Blocks.CRACKED_STONE_BRICKS.getBlock().defaultBlockState())){
                this.blockState = Blocks.CRACKED_STONE_BRICKS.getBlock().defaultBlockState();
                decayed = true;
                return this;
            }
        }
        //WorldDecayData.get();
        return null;
    }
}

 

This code works fine when I load in to a world and place stone bricks down. The stone bricks stay there for a couple of seconds and then they change into cracked stone bricks.
The problem occurs when I save and quit to title and rejoin the same world when I placed stone brick that hadn't decayed yet. The game seems to remember what DecayHandlers were running which I guess is logical because I never closed it.

So when the world is reloaded again my code stops without error and no longer functions. I can see in the terminal that the last message that was displayed was that a block decayed so I put a breakpoint on it to see what went wrong. By the way, the game keeps running just fine but my code just stops.
The part where it seems to go wrong is in this line: 

if(world.setBlockAndUpdate(blockPos, Blocks.CRACKED_STONE_BRICKS.getBlock().defaultBlockState()))

When I try to step in to every single detail the callstack becomes insanely large and I'm unable to understand what's going on.
The only thing I know is that right at the end they put my thread in to parking or something? I really don't understand what was going on.

Can someone explain why this is happening? I don't have a clue of what's going on.

Edit: Here is a GitHub link of the project: https://github.com/Astro2202/DecayMod

Edited by Astro2202
  • Astro2202 changed the title to My code stops working and iterating without any error message
Posted (edited)

And how do you call this class and its method?
Your thread title says iteration, but I do not see a loop.
Why aren't you using the scheduled block tick system?
What happens if the random isn't 0 and your tick function returns?

7 hours ago, Astro2202 said:

The problem occurs when I save and quit to title and rejoin the same world

No shit sherlock. The game doesn't magically know how to save your miscellaneous runtime class to the save file.

Edited by Draco18s

Apparently I'm a complete and utter jerk and come to this forum just like to make fun of people, be confrontational, and make your personal life miserable.  If you think this is the case, JUST REPORT ME.  Otherwise you're just going to get reported when you reply to my posts and point it out, because odds are, I was trying to be nice.

 

Exception: If you do not understand Java, I WILL NOT HELP YOU and your thread will get locked.

 

DO NOT PM ME WITH PROBLEMS. No help will be given.

Posted (edited)
9 hours ago, Draco18s said:

And how do you call this class and its method?

The class is initialized in the BlockEvent.EntityPlaceEvent.
EntityPlaceEvent

@SubscribeEvent
    public static void onBlockPlace(final BlockEvent.EntityPlaceEvent placeEvent){

        BlockState blockState = placeEvent.getPlacedBlock();
        BlockPos pos = placeEvent.getPos();
        System.out.println("placement");

        if(blockState.getBlock().equals(Blocks.STONE_BRICKS)){
            System.out.println("Stonebrick placed");
            DecayHandler decayHandler = new DecayHandler(blockState, pos, world);
            decayHandlers.add(decayHandler);
        }
    }

The OnTick method is called in the TickEvent.WorldTickEvent.
WorldTickEvent:
 

 @SubscribeEvent
    public static void worldTickEvent(final TickEvent.WorldTickEvent tickEvent){
        if(world == null){
            world = tickEvent.world;
        }

        List<DecayHandler> decayHandlersToBeRemoved = new ArrayList<>();

        for(DecayHandler decayHandler: decayHandlers){
            DecayHandler decayHandlerToBeRemoved = decayHandler.OnTick();

            if(decayHandlerToBeRemoved != null){
                decayHandlersToBeRemoved.add(decayHandlerToBeRemoved);
            }
        }

        if(!decayHandlersToBeRemoved.isEmpty()){
            for(DecayHandler decayHandlerToBeRemoved : decayHandlersToBeRemoved){
                DecayHandler temp = null;
                for(DecayHandler decayHandler : decayHandlers){
                    if(decayHandler.equals(decayHandlerToBeRemoved)){
                        temp = decayHandler;
                    }
                }
                if(temp != null){
                    decayHandlers.remove(temp);
                }
            }
        }
        //worldDecayData.get(world);
    }

Here are the variables used in these events:

public static List<DecayHandler> decayHandlers = new ArrayList<>();
public static World world;

Also what might be relevant, the DecayHandler is also removed when the stone brick is removed ingame:
BreakEvent:

@SubscribeEvent
    public static void onBlockDestroy(final BlockEvent.BreakEvent breakEvent){
        BlockPos blockPos = breakEvent.getPos();
        DecayHandler decayHandlerToBeRemoved = null;
        System.out.println("broke block @ " + blockPos);
        for(DecayHandler decayHandler : decayHandlers){
            System.out.println("decayHandler location: " + decayHandler.blockPos);
            if(decayHandler.blockPos.equals(blockPos)){
                decayHandlerToBeRemoved = decayHandler;
            }
        }
        if(decayHandlerToBeRemoved != null){
            decayHandlers.remove(decayHandlerToBeRemoved);
            System.out.println("decayHandler Removed");
        }
    }

 

9 hours ago, Draco18s said:

Your thread title says iteration, but I do not see a loop.

Perhaps I misused the term iteration.. The code should be running constantly without interruption but just stops without error or warning. It's because while this problem occurs, there are always DecayHandlers active that are called in every WorldTickEvent.

9 hours ago, Draco18s said:

Why aren't you using the scheduled block tick system?

I unfortunately have never seen anything about a scheduled block tick system. Thank you for bringing it to my attention.

9 hours ago, Draco18s said:

What happens if the random isn't 0 and your tick function returns?

When it isn't 0, the block will not decay and thus nothing will happen to the block. Like you can see in the code above, blocks that have decayed need to be removed out of a list and when the block has decayed it returns itself so that it can be identified which DecayHandler to remove. If it hasn't decayed, it returns null and so it will not be removed.

I know there is some incredibly inefficient code here but right now i'm just trying to work out my idea and correctly implement it later.

 

9 hours ago, Draco18s said:

No shit sherlock. The game doesn't magically know how to save your miscellaneous runtime class to the save file.

I've actually been trying to save this information for a couple of days now and actually have a thread about this that I posted not long before this one. It's when trying to make saving work that I discovered this problem. When I comment out all my code used to try and save data (right now I'm trying to use WorldSavedData by the way), the problem still occurs and I thought it was an unrelated issue that I need to fix first. So if I understand correctly, I should try and save my classes and stop running them while quitting a world. Because the class keeps running even when the world isn't loaded, which causes the issue when I try and reload the world? Because the decay happens at a random moment, sometimes the code keeps running for a couple of seconds even though I'm already loaded back in to the world. It's just as soon as a block needs to be updated that the code stops even though the stone brick that was originally placed is fully loaded in and all the parameters of the DecayHandler class are still correct.

Maybe some important information, I do have a good understanding of Java. It's just that I don't really have a lot of experience with Forge and don't have a full understanding of it yet. But I'm eager to learn!
Thank you for your response, I realize that I'm probably making stupid mistakes and don't see the obvious, but I hope that's normal for a Forge rookie.
 

Edited by Astro2202
  • Astro2202 changed the title to My code stops working without any error message
Posted
2 hours ago, Astro2202 said:

When it isn't 0, the block will not decay and thus nothing will happen to the block. Like you can see in the code above, blocks that have decayed need to be removed out of a list and when the block has decayed it returns itself so that it can be identified which DecayHandler to remove. If it hasn't decayed, it returns null and so it will not be removed.

Thank you for that code, so that I could understand why you were returning values the way you were. The correct way to do this would be to return a boolean.

        for(DecayHandler decayHandler: decayHandlers){
            if(decayHandler.OnTick()){
                decayHandlersToBeRemoved.add(decayHandler);
            }
        }

But as I said, this whole custom data structure is unnecessary.

Quote

DecayHandler temp = null;

...The heck? You don't need this entire block. Much less two loops and a temporary variable!

decayHandlers.removeAll(decayHandlersToBeRemoved);

 

2 hours ago, Astro2202 said:

I should try and save my classes and stop running them while quitting a world.

Yes, but also no.

Yes, in the sense that you do need to have a way to serialize your data, but in 99% of cases you should simply be providing a method to serialize and let the game decide when to call it, via the Capabilities system. The structure you have here is a world capability, assuming the existing systems didn't do what you needed them to sufficiently.

Speaking of:

Quote

public static World world;

You know there's more than one world, right? Even in single player you have the ClientWorld, the Overworld, the Nether, and the End.

2 hours ago, Astro2202 said:

I unfortunately have never seen anything about a scheduled block tick system. Thank you for bringing it to my attention.

Hashtag I wonder how redstone repeaters work.

  • Like 1

Apparently I'm a complete and utter jerk and come to this forum just like to make fun of people, be confrontational, and make your personal life miserable.  If you think this is the case, JUST REPORT ME.  Otherwise you're just going to get reported when you reply to my posts and point it out, because odds are, I was trying to be nice.

 

Exception: If you do not understand Java, I WILL NOT HELP YOU and your thread will get locked.

 

DO NOT PM ME WITH PROBLEMS. No help will be given.

Posted
8 minutes ago, Draco18s said:

You know there's more than one world, right? Even in single player you have the ClientWorld, the Overworld, the Nether, and the End.

I do, but haven't put much though into it yet. I was trying to make it work in the overworld and worry about other worlds later.

Thank you for your response. It seems that I have a lot of homework to do.
I will take your feedback in to consideration and implement it accordingly. This might take a while but I'll update the thread if I either fix everything or get stuck again.

Thank you for your time!

Posted
On 5/19/2021 at 3:22 PM, Draco18s said:

The structure you have here is a world capability

So I've done some digging in to the capabilities system and I think I'm finally starting to understand it. I've tried to make use of the AttachCapabilitiesEvent<World> to add a capability with my DecayHandler class in it. When I would place a block I would set the decayhandler of that capability with the values of the block I placed. But there are two problems here.

1) I'm unsure of when and how many times the AttachCapabilitiesEvent<World> event is called. It could be that there is only one instance of this capability for what I know. Meaning that there also could be only one DecayHandler which there can't of course. The amount of decayhandlers available also shouldn't be defined by the amount of times that the AttachCapabilitiesEvent<World> is called but by the actual blocks that you place.

2) Correct me if I'm wrong, but when a capability is added, you give it a provider which gives you a default class of what you want to save. I need to provide these default values myself but I can't just give a default blockstate and position so the values are null and because of that the game will crash because of a nullpointerexception.

So here's what I'm thinking. I can create a capability with simply a list of decayhandlers that is initially empty that is added with the AttachCapabilitiesEvent<World> event. Then on the BlockEvent.EntityPlaceEvent event I can create the decayhandler and add it to the list within the capability. The world will remember all the decayhandlers because when the writeNBT method is called, I can simply iterate over the list of decayHandlers and save the relevant data. When they need to be loaded back in I can simply create the decayhanders again with the data that was saved, and add them back to the list within the capability.

Is this a valid idea?
 

On 5/19/2021 at 3:22 PM, Draco18s said:

assuming the existing systems didn't do what you needed them to sufficiently.

Also I'm not sure with what you mean here. Isnt the world capability an existing system? Or do you mean the capabilities provided by forge e.g. IItemHandler?

Can you also please explain when and how many times AttachCapabilitiesEvent<World> is called?

Last thing, I appreciate your posts! They really set me in the right direction. It's hard to find relevant information and it's often outdated or conflicting with other sources. At least in your comments I can have complete trust.

Posted
1 hour ago, Astro2202 said:

how many times the AttachCapabilitiesEvent<World> event is called

Once per world when the world is loaded.

1 hour ago, Astro2202 said:

but by the actual blocks that you place.

The block can't store data about the decay process, because blocks are singletons. Your options are either (a) a tile entity or (b) world capability data (that knows about the positions and times). Use world.getCapability to get your capability and store/retrieve/update the data as needed. Your capability will still be a map of postions -> times.

1 hour ago, Astro2202 said:

you give it a provider

The World class is already a capability provider, you don't need to create your own unless you want to have a capability attached to an option that is not already a capability provider. Worlds, chunks, entities (including tile entities and players), and itemstacks are all capability providers.

1 hour ago, Astro2202 said:

<general process>

Is this a valid idea?

Pretty much. I don't think you need your DecayHandler class, as your capability class is your DecayHandler. It just needs to store a map of postion->time. Depending on how you want to track that time (every tick? only when the block gets an update tick?) might alter the implementation details a little, but in general it's just a list of positions and the decay value. Or possibly just a list of block positions if no actual time data is relevant (the block would just query the capability to see if it should decay or not, and if so, remove its position from the capability).

1 hour ago, Astro2202 said:

Also I'm not sure with what you mean here. Isnt the world capability an existing system? Or do you mean the capabilities provided by forge e.g. IItemHandler?

I mean the scheduled tick system. world.scheduleBlockUpdate or something like that. You give it a position and a number of ticks to wait, and your block will have its updateTick method called at that time.

  • Like 1

Apparently I'm a complete and utter jerk and come to this forum just like to make fun of people, be confrontational, and make your personal life miserable.  If you think this is the case, JUST REPORT ME.  Otherwise you're just going to get reported when you reply to my posts and point it out, because odds are, I was trying to be nice.

 

Exception: If you do not understand Java, I WILL NOT HELP YOU and your thread will get locked.

 

DO NOT PM ME WITH PROBLEMS. No help will be given.

Posted (edited)

Ok so this might be a lengthy one. (I think) I succeeded in fully implementing the capability and adding the implementation mentioned in my previous comment. I also removed the need of a decayhandler class and all logic that happened in the DecayHandler now takes places in the capability.

The problem is that I'm experiencing the same issue that originally made me open this thread.
The code stops running without error when I reload the world.

I will post all relevant classes and carefully explain what the problem is.

To start with: When I load a world, AttachCapabilitiesEvent<World> get's fired and adds a capability to the world provider. <- this sentence is probably false but don't know how to put it.
This event gets fired in a class called DecayEventHandler.java:
 

@Mod.EventBusSubscriber(modid = DecayMod.MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE)
public class DecayEventHandler {

    @SubscribeEvent
    public static void onAttachingCapabilitiesEvent(final AttachCapabilitiesEvent<World> event){
        if(event.getObject() instanceof World){
            DecayProvider provider = new DecayProvider();
            event.addCapability(new ResourceLocation(DecayMod.MOD_ID, "decayhandler"), provider);
            event.addListener(provider::invalidate);
        }
    }

    @SubscribeEvent
    public static void onBlockPlaceEvent(BlockEvent.EntityPlaceEvent event){
        World world = event.getEntity().getCommandSenderWorld();
        System.out.println("Placement");
        world.getCapability(DecayCapability.DECAY_CAPABILITY).ifPresent(h -> {
            System.out.println("Capability is present");
            if(event.getPlacedBlock().equals(Blocks.STONE_BRICKS.defaultBlockState())){
                System.out.println("DecayHandler for stone brick initialized");
                h.addBlock(event.getPos());
            }
        });
    }
}

As you can see, the capability is added and given a provider. To be honest I am not really sure why and how everything works. I've based this on multiple tutorials, guides and documentation to come up with this. But anyway, the provider is an instance of DecayProvider.java:
 

public class DecayProvider implements ICapabilitySerializable<CompoundNBT> {
    private final DefaultDecay decayHandler = new DefaultDecay();
    private final LazyOptional<IDecay> decayHandlerOptional = LazyOptional.of(() -> decayHandler);

    public void invalidate(){
        decayHandlerOptional.invalidate();
    }

    @Nonnull
    @Override
    public <T> LazyOptional<T> getCapability(@Nonnull Capability<T> cap, @Nullable Direction side) {
        return decayHandlerOptional.cast();
    }

    @Nullable
    @Override
    @SuppressWarnings("unchecked")
    public CompoundNBT serializeNBT() {
        if(DecayCapability.DECAY_CAPABILITY == null){
            return new CompoundNBT();
        }else{
            return (CompoundNBT) DecayCapability.DECAY_CAPABILITY.writeNBT(decayHandler, null);
        }
    }

    @Override
    public void deserializeNBT(CompoundNBT nbt) {
        if(DecayCapability.DECAY_CAPABILITY != null){
            DecayCapability.DECAY_CAPABILITY.readNBT(decayHandler, null, nbt);
        }
    }
}

This provider contains the DefaultDecay.java class. this class contains all decay logic and a list of block positions where it should apply the decay logic on.
 

public class DefaultDecay implements IDecay {

    private List<BlockPos> blockPosList;
    private List<BlockPos> blocksToBeRemoved;
    private World world;
    private Random random;
    private int decayChance;

    public DefaultDecay(){
        this.blockPosList = new ArrayList<>();
        this.blocksToBeRemoved = new ArrayList<>();
        this.random = new Random();
        this.decayChance = 1000;
        MinecraftForge.EVENT_BUS.register(this);
    }

    @Override
    public List<BlockPos> getAllBlockPositions() {
        return this.blockPosList;
    }

    @Override
    public void addBlock(BlockPos blockPos) {
        blockPosList.add(blockPos);
    }

    @Override
    public void removeBlock(BlockPos blockPosToRemove) {
        BlockPos tempBlockPos = BlockPos.ZERO;
        for(BlockPos blockPos : blockPosList){
            if(blockPos.equals(blockPosToRemove)){
                tempBlockPos = blockPos;
            }
        }
        if(!tempBlockPos.equals(BlockPos.ZERO)){
            blockPosList.remove(tempBlockPos);
        }
    }

  	@SubscribeEvent
    public void onWorldTick(final TickEvent.WorldTickEvent event){
        if(this.world == null){
            this.world = event.world;
        }

        for(BlockPos blockPos : blockPosList){
            if(random.nextInt(decayChance) == 0){
                System.out.println(blockPos + " is Decaying");
                if(world.setBlockAndUpdate(blockPos, Blocks.CRACKED_STONE_BRICKS.getBlock().defaultBlockState())){
                    this.blocksToBeRemoved.add(blockPos);
                    System.out.println(blockPos + " Decayed");
                }
            }
        }
        
        blockPosList.removeAll(blocksToBeRemoved);
        blocksToBeRemoved.clear();
    }
}

This class implements the IDecay.java interface
 

public interface IDecay {
    List<BlockPos> getAllBlockPositions();
    void addBlock(BlockPos blockPos);
    void removeBlock(BlockPos blockPos);
}

DecayProvider.java also states which capability is used and saved. As you can see in the class posted above it states "DECAY_CAPABIITY"
Now here is the DecayCapability.java class:
 

public class DecayCapability {
    @CapabilityInject(IDecay.class)
    @SuppressWarnings("ConstantConditions")
    public static Capability<IDecay> DECAY_CAPABILITY = null;

    public static void registerCapabilities(){
        CapabilityManager.INSTANCE.register(IDecay.class, new Storage(), DefaultDecay::new);
    }

    public static class Storage implements Capability.IStorage<IDecay>{

        @Nullable
        @Override
        public INBT writeNBT(Capability<IDecay> capability, IDecay instance, Direction side) {
            final CompoundNBT nbt = new CompoundNBT();
            int counter = 0;
            for(BlockPos blockPos : instance.getAllBlockPositions()){
                nbt.putInt("xPos" + counter, blockPos.getX());
                nbt.putInt("yPos" + counter, blockPos.getY());
                nbt.putInt("zPos" + counter, blockPos.getZ());
                counter++;
            }
            nbt.putInt("amount", counter);
            return nbt;
        }

        @Override
        public void readNBT(Capability<IDecay> capability, IDecay instance, Direction side, INBT nbt) {
            for(int i = 0; i < (((CompoundNBT) nbt).getInt("amount")); i++){
                BlockPos blockPos = new BlockPos(((CompoundNBT) nbt).getInt("xPos" + i), ((CompoundNBT) nbt).getInt("yPos" + i), ((CompoundNBT) nbt).getInt("zPos" + i));
                instance.addBlock(blockPos);
            }
        }
    }
}

The capability is registered and it's here that I specified how to save and load all the positions of the DefaultDecay instance.
The method that registers the capability is spoken to in the FML CommonSetupEvent
 

private void setup(final FMLCommonSetupEvent event)
    {
        System.out.println("Init!");
        DecayCapability.registerCapabilities();
    }

I think I have given all the code that is relevant.
Now everything works fine when I first load in to the world. I get all messages in the console that I wrote down. I place Stone bricks and they decay. All good.
It's when I save and quit the world, then rejoin the world that it starts breaking. Here is what is last printed in the console:

[23:03:03] [Server thread/DEBUG] [ne.mi.fm.FMLWorldPersistenceHook/WP]: Gathering id map for writing to world save New World
[23:03:03] [Server thread/DEBUG] [ne.mi.fm.FMLWorldPersistenceHook/WP]: ID Map collection complete New World
[23:03:07] [Server thread/INFO] [STDOUT/]: [com.astro.decaymod.core.capabilities.DefaultDecay:onWorldTick:68]: BlockPos{x=-180, y=84, z=63}is Decaying
[23:03:07] [Server thread/INFO] [STDOUT/]: [com.astro.decaymod.core.capabilities.DefaultDecay:onWorldTick:71]: BlockPos{x=-180, y=84, z=63}Decayed
[23:03:10] [Server thread/INFO] [STDOUT/]: [com.astro.decaymod.core.capabilities.DefaultDecay:onWorldTick:68]: BlockPos{x=-180, y=83, z=63}is Decaying

After this last line, nothing else is printed and the remaining blocks don't decay anymore.
The problem occurs in this line:

System.out.println(blockPos + "is Decaying");
if(world.setBlockAndUpdate(blockPos, Blocks.CRACKED_STONE_BRICKS.getBlock().defaultBlockState())){
  	System.out.println(blockPos + "Decayed");

In some rare cases - like coincidentally in this run like you can see in the console - there is 1 block that still decays just fine. But in most cases the first block that decays after the reload breaks the code just like the second one after the reload did now. It seems like the code steps in the setBlockAndUpdate method and never comes out. It just stops functioning.
I just now also noticed that ingame commands no longer work. Something has gone really wrong in the back and I don't know why. It could be because I haven't implemented the capability right. I only half know what I'm doing with it. I also realize that there is still a lot of code that needs improving like I still didn't make use of the scheduled block tick system.

So I'm thinking, I either didn't implement the saving right or it's an unrelated issue. What do you think @Draco18s?

Edit: I've done some further "testing" and when this problem occurs the whole server side seems to freeze. Mobs don't walk anymore, commands don't work anymore and when destroying the bottom half of tall grass the upper half does not destroy with it.
I first load in the world and place 4 stone bricks, they can decay like I coded. I place 4 stone bricks, quit and rejoin, and sometimes 1 but otherwise no stone bricks decay and as soon as the setBlockAndUpdate method is called the server freezes. Like you can see in the console, the block position given to this method is still correct. So to me it's then obvious the problem lies with the world object that I'm trying to call it on right? perhaps it's no longer the same object.

Edit 2: Ok so the problem is not solved but good news! When I fully quit the game and reboot and then load in the same world it is fully functional! It remembers what blocks I placed and need to decay and it doesn't freeze like it does when I reload the world without quitting the game. This is a big relief. So the saving at least somewhat works thanks to your feedback!
 

Edited by Astro2202
  • Astro2202 changed the title to [Solved] [1.16.5] My code stops working without any error message

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.

Announcements



×
×
  • Create New...

Important Information

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