Skip to content

Instantly share code, notes, and snippets.

@gabizou
Last active June 18, 2018 03:37
Show Gist options
  • Save gabizou/e4d0e9e7fee420612c3519e14c450f16 to your computer and use it in GitHub Desktop.
Save gabizou/e4d0e9e7fee420612c3519e14c450f16 to your computer and use it in GitHub Desktop.

PhaseTracker

Current state is still in development

#####Preface By this point, you should already understand the object structure and concept of Causes and EventContexts. If you have not, do consider reading that aspect prior to reading this section as this section is about the implementation that powers creating these objects for all events.

Topics required to understand this section include but are not limited to:

  • Causes
  • Stacks (Data structure)
  • Lists (Data structure)
  • MultiMaps (Yet another data structure)
  • Mixins (Sponge topic)

Before I can talk about how we create Causes, I have to talk about Stacks

#####If you already know about stacks and understand Java stack traces and how Java is a stack based system, skip to the next section Consider for a moment, you write a simple plugin and in your listener, you have an exception, usually what helps you understand where the exception came from, Java provides you with a stacktrace. Fortunately for you, the smart developer you are, can decipher a stack trace and understand that at the top of the stack trace is the culprit for the exception to be thrown! Now, how did Java get to tell you about this? Well, Java uses a stack based process so that when a method calls another method, method 1 is pushed onto the stack (for being called first), and then method2 is pushed onto the top of the stack for being called, so in the "stack" you'd have:

method2()
method1()
bunchOfJavaNative()
javaLaunchWhatever()

Fortunately, we can derive that stack traces basically tell us who and where an exception is caused, and the "trace" of how we got there. Now, imagine that we started applying this "call stack" to Minecraft to trace who is doing what.

Sure, I already know about call stacks, but how does that apply to Minecraft?

Well, I did say imagine, didn't I? If we were to consider for a second, that since Minecraft pretty much runs everything on the "server thread", we can consider it's in a constant global state of going back and forth between ticking a World, which will tick Entities, TileEntities, and Blocks, each of which has a potential to do something, we can start to consider that Minecraft is yet another stack of things that we can trace. Now if only we could actually trace who is doing what and react accordingly....

Ok.... But how does a state tracker make causes work?

Ok, I know, I'm getting to the point, just have to break down what literally powers all events in Sponge to this doc, so forgive me if I'm writing for everyone to be able to understand... Anyways, the reason why we talk about treating Minecraft as a stack and tracing who is doing what is that we literally do just that.

To show you, I have to present three classes to you:

somehow make these classes foldable or something?
  • PhaseTracker
  • IPhaseState
  • PhaseContext
public final class PhaseTracker {
    
    private static final PhaseTracker INSTANCE = new PhaseTracker();
    
    private PhaseTracker() {
    }
    
    public static PhaseTracker getInstance() {
        return INSTANCE;
    }
    
    public PhaseData getCurrentPhaseData() {
        return this.stack.peek();
    }
    
    public IPhaseState<?> getCurrentState() {
        return this.stack.peekState();
    }
    
    public PhaseContext<?> getCurrentContext() {
        return this.stack.peekContext();
    }
    
    void switchToPhase(IPhaseState<?> state, PhaseContext<?> phaseContext) {
        // something something something dark side
        // sure, could show the actual implementation, but what's the point of that?

        this.stack.push(state, phaseContext);
    }

    
    public void completePhase(IPhaseState<?> prevState) {
        // Lots of code here, but we'll go over it a little later.
    }
}

In a sense, PhaseTracker is what is usually referred to when it comes to Sponge's tracking system, since literally everything flows through it. It's a static global state stack manager in a few words, and there's about three methods (really two) that are ever used: getCurrentState() and getCurrentContext(). he other two methods are realistically internalized to the IPhaseStates own self closing selves (which I'll talk about later). What PhaseTracker can be summarised as: It has a stack of IPhaseStates and their respective PhaseContexts to where each IPhaseState is processed when it is being "popped".

To give some understanding of IPhaseState, it's the equivalent to a literal GameState for the game state events where we "know" something is happening, like when an Entity is ticking. We achieve this "state" of knowing by using Mixins where they are necessary.

To give you the specific example of how we know an Entity is ticking, we (developers) already know that Minecraft ticks each World, which ticks each Entity currently loaded. So, if we write the following mixin:

@Mixin(net.minecraft.world.World.class)
public class MixinWorld {
    
    @Redirect(method = "updateEntityWithOptionalForce", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/Entity;onUpdate()V"))
    protected void onCallEntityUpdate(net.minecraft.entity.Entity entity) {
        entity.onUpdate(); // We can call the same method
    }
}

Wait, where did we go from IPhaseStates to Mixins?

Sorry, I got a little ahead of myself.... We have to discuss the case where IPhaseStates are very specific, if we want to say that an Entity is ticking, we have to know that the current IPhaseState is a specific state, right?

Well, we can create static singleton states, like EntityTickPhaseState and have a singular reference to said state, which we do in TickPhase.

This new EntityTickPhaseState is neat and all, but we need to actually discuss PhaseContext a little to give some understanding before I proceed.

PhaseContext is a rudimentary "state context". Take our first example of your method1() calling method2() with specific arguments, and if one of the arguments is known to cause issues, you'd print out a warning about the argument, right? Right. That's traditional "warn that the provided inputs are not valid and exit safely". We would like to be able to harness the ability to say that "hey, we know which entity is ticking during this IPhaseState." And this is where we can start using PhaseContext to literally store whatever we need during a IPhaseState, for later retrieval during the current state, such as when we're throwing events....

How do we go from a PhaseContext to events? The whole point of this topic is to explain how we implement our events and their causes, this is it. Since the PhaseContext stores some information for us (by our own design, not magically), we can retrieve that information as long as it is relevant to whatever we need to do at the time.

So, if I'm throwing an event and PhaseTracker.getInstance().getCurrentState() returns an instance of EntityTickPhaseState, we know that there would be an Entity as the source for ticking, and therefor can do the following:

public void someMethodCalledDuringATick() {
    if (PhaseTracker.getInstance().getCurrentState() instanceof EntityTickPhaseState) {
        final Entity entity = PhaseTracker.getInstance().getCurrentContext().getSource(Entity.class);
        Sponge.getCauseStackManager().pushCause(entity);
        // do something that will throw an event, or throw your own event, and use the CauseStackManager to get your current cause
    }
}

Huh, so you use the states to check what to push to the CauseStackManager?

Yep, the states already know what they are assigned to do, and when to do them, but it's not as hard coded as the above example, it's a little more streamlined than that. Since Sponge has to remain super abstract about most things, Sponge will delegate a majority of processing to the actual IPhaseState, which essentially writes it's own contract on what should be available, made available, and how to process whatever is being captured, performed, thrown, or changed.

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