Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Cause Enhancements #712

Closed
wants to merge 1 commit into from
Closed

[WIP] Cause Enhancements #712

wants to merge 1 commit into from

Conversation

gabizou
Copy link
Member

@gabizou gabizou commented Jun 3, 2015

So far, Cause as it stands is somewhat confusing with the inclusion of Reason such that it isn't clear as to what should be considered a Cause and what should be considered a Reason.

This PR aims to achieve the following:

  • Cause becomes a container object of all related objects associated with the cause for a CauseTracked event
  • "Reasons" or additional causes aiding the direct cause for an event are included in Cause, the closer the association to the direct cause, the lower the index that object will be
  • Include EntityDamageEvent and DamageSources while keeping in line with the primary Cause goal. Any additional associated objects aiding the direct DamageSource are included in the Cause associated with the EntityDamageEvent
  • Introduce EntityPreDamageEvent where the damage calculations for modifying the raw damage are included, this is primarily used for modifying the outgoing damage BEFORE the EntityDamageEvent is fired, this is equivalent to having an event to modify the damage a Player will output when using an ItemStack of ItemTypes.DIAMOND_SWORD such that all modifiers are included (such as the enchantments on the sword, the potion effects, and any other AttributeModifiers or alternative DamageModifiers relating to the final raw damage output from the Player
  • For many CauseTracked events, the Cause now is always available, but a Cause.empty() may be included

So what does this really mean for CauseTracked events?

What this means is that a vanilla interaction throwing a CauseTracked event will have as much information as possible within the Cause. With the following addition to Cause:

<T> Optional<T> getFirst(Class<T>);

<T> Optional<T> getLast(Class<T>);

a plugin is easily able to determine whether a BlockState was the cause for an event, a Player, or if a TileEntity was part of the mix.

The one issue with Cause previously was that it was always optional. Passing in a null to the SpongeEventFactory is that the Cause is always optional, even when it should not be. In the PR, I change this to where Cause can be "empty". Some might think: Wait! Isn't that making Cause just become a similar object to Optional? To which the answer is yes. The one issue with Optional however is that Optional can only hold a single value. There is no way to hold multiple values, except if the Optional is declared like so: Optional<Object[]>, which eliminates any utility methods provided by getFirst(Class<T>) and getAllOf(Class<T>) from Cause.

Ok, so where does this come into play with actual events? I want to know the cause for a BlockChangeEvent!

Well, see, this is where the uniqueness comes into play. Since we always have a Cause, we can always call getFirst(Player.class) which will return a present Player if and only if a Player was involved in the BlockChangeEvent. How the objects are ordered in the Cause is simple: The direct cause of the event comes first, any additional "helpers" that aid the root cause are indexed afterwards.

I'll give an example here of how a BlockChangeEvent could have the order of objects in the cause, for the case of a Player placing a ItemTypes.SAPPLING.

First, the Player is the root cause, because the player is right clicking with the ItemStack.
Second, the ItemStack follows because the ItemStack is a part of the "reason" why the BlockChangeEvent is being called.

Now, what I haven't seen is how this affects something like EntityDamageEvent, care to elaborate?

Of course! With an EntityDamageEvent, I'll have to pull the same description from #707

Skeleton shoots bow launching an arrow hitting a Player.

This is pretty plain and simple to understand what happens, but we need to look at it much deeper to fully understand what the final damage comes out to:

I'll give an order of how the damage is calculated:

  1. Skeleton is shooting the arrow, by default it will apply say 3 damage to the Arrow
  2. The bow, if enchanted applies a bonus amount of damage to the arrow, say 1.5 additional damage
    The final raw damage of the arrow is now 4.5.

Now here comes the fun part:
The Player is hit with the arrow

  1. The damage is modified based on the game difficulty, in this case, it's on HARD difficulty, so the damage is multiplied by 1.5 (damage is now 6.75)
  2. Armor calculations are factored in, Say that the player has full iron armor, and the iron armor reduces the damage by 2 (damage is now at 4.75)
  3. Potion effects add to resistances, so damage is reduced by multiplying damage by 20 and dividing by 25 (damage is now at 3.8)
  4. Armor Enchantments reduce the damage by 20% for example (the damage is now at 3.04)
  5. The Absorption potion effect is now factored in to find out how much actual health is removed from the entity, say we have an additional 2 health from absorption, (damage is now at 1.04)

The end result is a damage value of 1.04 damage dealt directly to the entity, where 4.5 damage originated.

The reason why I walked you through all of this is the following: I've been contemplating that we should have a LivingPreAttackEntityEvent of which we can trace the modifiers for the damage being dealt to the entity (in the case of the skeleton shooting, there's the modifier of the bow and the modifier base damage of the skeleton). After that event, we'd need a proper WeightedCollection to store the DamageModifiers and their Functions being applied to the incoming damage for an EntityDamageEvent. Considering the limitless possibilities of being able to identify the DamageModifiers and the Cause for each modifier, I do believe this would make an excellent change with Causes PR in the near future.

What HAS been achieved however, is slightly different. Instead of just including the associated Entity as a root to the Cause, a DamageSource is now the root cause. A DamageSource is essentially a wrapper around any object with a descriptive DamageType with various attributes of the DamageSource including whether the damage is absolute, ignores armor, magical, explosive, or even scaled with difficulty. The included DamageSources in this PR facilitate understanding the type of damage being dealt, and the parties involved in the DamageSource.

I'm a little confused about DamageSource, can you give me an example?

Of course! Say you have an EntityPreDamageEvent:

The DamageSource in this case is actually a ProjectileDamageSource, where the Projectile is an Arrow, and the ProjectileSource is a Skeleton. Given that the ProjectileDamageSource is the root cause to the EntityPreDamageEvent, we can then use this information to know that the Cause should have a related ItemStack used as a bow to shoot the Arrow. With the bow, we know that it can have no Enchantments or it can have some Enchantments. Of course, with these objects in the Cause, it makes it plain and clear to use them:

@Subscribe
public void cancelAnvils(EntityPreDamageEvent event) {
    if (event.getCause().isEmpty()) {
        return;
    }
    final Cause cause = event.getCause();
    final Optional<BlockDamageSource> damageSource = cause.getFirst(BlockDamageSource.class);
    if (damageSource.isPresent()) {
        if (damageSource.get().getBlockState().getType() == BlockTypes.ANVIL) {
            event.setCancelled(true);
        }
    }
}

@Subscribe
public void skeletonArrows(EntityPreDamageEvent event) {
    if (event.getCause().isEmpty()) {
        return;
    }
    final Cause cause = event.getCause();
    final Optional<ProjectileDamageSource> projectileDamageSource = cause.getFirst(ProjectileDamageSource.class);
    if (projectileDamageSource.isPresent()) {
        final ProjectileSource source = projectileDamageSource.get().getShooter();
        if (source instanceof Skeleton) {
            final SkeletonType skeletonType = ((Skeleton) source).getData(SkeletonData.class).get().getValue();
            if (skeletonType == SkeletonTypes.WITHER) {
                event.setDamageFunction(new MyDamageModifier(Cause.of(source, this)), new Function<Double, Double>() {
                    @Nullable
                    @Override
                    public Double apply(Double input) {
                        return input * 3 / 2;
                    }
                });
            } else if (skeletonType == SkeletonTypes.NORMAL) {
                event.setDamageFunction(new MyDamageModifier(Cause.of(source, this)), new Function<Double, Double>() {
                    @Nullable
                    @Override
                    public Double apply(Double input) {
                        return input * 3 / 4;
                    }
                });
            }
        }
    }
    for (final Tuple<DamageModifier, Function<? super Double, Double>> damageModifier : event.getModifiers()) {
        if (damageModifier.getFirst().getCause().getFirst(ItemStack.class).isPresent()) {
            // We know that we have an ItemStack that was used in the DamageModifier cause, we can suspect that
            // the ItemStack has some sort of enchantments, or not
            final ItemStack itemStack = damageModifier.getFirst().getCause().getFirst(ItemStack.class).get();
            final Optional<EnchantmentData> dataOptional = itemStack.getData(EnchantmentData.class);
            if (dataOptional.isPresent() && dataOptional.get().get(Enchantments.POWER).isPresent()) {
                event.setDamageFunction(damageModifier.getFirst(), new Function<Double, Double>() {
                    @Nullable
                    @Override
                    public Double apply(@Nullable Double input) {
                        return input * 1.25 * dataOptional.get().get(Enchantments.POWER).get().doubleValue();
                    }
                });
            }
        }
    }
}

public final class MyDamageModifier implements DamageModifier {
    private Cause cause;

    public MyDamageModifier(Cause cause) {
        this.cause = checkNotNull(cause);
    }

    @Override
    public Cause getCause() {
        return this.cause;
    }

    @Override
    public String getId() {
        return "com.gabizou.MyDamageModifier";
    }

    @Override
    public String getName() {
        return "MyDamageModifier";
    }
}

Holy cow batman! Why so complicated?!

Unfortunately, there is A LOT that goes into an EntityDamageEvent, even more so if you want to be able to manipulate the damage coming in, essentially the EntityPreDamageEvent. To fully unlock the power available with this, the possibilities to associate new DamageModifiers with a damage event are nigh limitless. Since we can expose the underlying functions, as they placed in order from vanilla mechanics, we can successfully interpret that an EntityPreDamageEvent is augmented not only by the Skeleton, but also that the ItemStack used by the Skeleton affects the total damage applied onto the Arrow. With that in mind, we can also assume that we can interpret the following EntityDamageEvent with similar processing for DamageModifiers when an Entity is being damaged, dictating the final damage actually being applied to the Entity, and not just interpret the raw incoming damage.

Of course, there are so many factors that can go into the EntityDamageEvent that I can't possibly explain it all in a simple code snippet, but I can explain in layman terms here.

With the EntityDamageEvent being fired only while the Entity is being damaged, we can gather all DamageModifiers that would further process the raw incoming damage, such as ItemStacks that are considered to be armor, PotionEffects that are absorbing damage, heck, we can even apply our own custom DamageModifiers if we had a party plugin that had a "reducing damage" effect based on the number of players joined in the party! All of this is as extensible as possible.

Ok... well, it's clear that you've given a lot of thought about EntityDamageEvent, but what about other CauseTracked events?

Similar to the EntityDamageEvent and EntityPreDamageEvent, we can safely say that most CauseTracked events are going to receive similar treatment with regards to adding additional sources/causes/reasons whatever you want to call them. So far, I've only been able to seriously work on the damage events, however, my thought process is that with EntitySpawnEvent, it is very easy to apply the same idea of having the following:

EntitySpawnEvent:

  • SpawnCause : An abstract root cause that aids in helping further understand the Cause for why an Entity is being spawned
  • SpawnType : Another CatalogType that aims to further simplify the type of SpawnCause, more specifically, when an Entity is spawned by a MobSpawner, the SpawnCause would be an instance of a MobSpawnerSpawnCause with the associated MobSpawnerData that defined the Entity to be spawned

EntityTeleportEvent:

  • TeleportType : Yet again, a helper CatalogType that aims to make it understandable why an Entity is being teleported
  • TeleportCause : Another root cause for the Entity to be teleporting, can range from EntityTeleportCause (an Enderman teleporting due to rain), TeleporterTeleportCause (a portal performing the teleport as per vanilla mechanics)

The list will grow as the PR is worked on and time goes on.

So, will this make it easy for me to know if a BlockChangeEvent was caused by a Player using some bonemeal on a sapling?

In a short answer: yes.
The long answer: The Cause would indeed have a Player object, and the secondary cause would be the ItemStack that "aided" in the cause for the BlockChangeEvent to take place. What is better is that with the knowledge of the Player, the ItemStack, and any other objects placed into the Cause, it becomes very simple for you to decide on changing the BlockChangeEvent for whatever purpose.

How about throwing custom events? I don't really understand what I'm supposed to do here if I want to allow plugins to hook into my custom EntityTeleportEvent.

To be honest, that is why I simplified Cause to replicate Optional. When you are wanting to teleport an Entity with a Cause, you can include the root cause, say the Entity called a Command, and the Command was entered through a Sign, you now have two objects to include in your Cause. However, let's say you wanted to expand and provide some custom TeleportCauses, you could extend TeleportCause and provide the extended Cause to be the Sign, and likewise you could include your own custom marker object so that in the event you have a listener for EntityTeleportEvent, you can safely ignore the event if your custom marker object is included.

How about X event?

Well, as I said before, the PR is still a work in progress, so I haven't finished designing the Cause for every CauseTracked event.

public static final DamageType FIRE = null;
public static final DamageType MAGIC = null;
public static final DamageType PROJECTILE = null;
public static final DamageType PLUGIN = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer the name CUSTOM

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed - if a mod uses a new damage type, it would likely use this one, and mods != plugins.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a mod uses a new damage type, it wouldn't be PLUGIN, it would have it's own id and everything.

@ryantheleach
Copy link
Contributor

Would/Could these tie into your PR?

@Faithcaio
Copy link
Contributor

BlockMoveEvent, EntityExplosionEvent and EntityDeathEvent(or maybe a separate event for breaking Vehicles/Hanging or others that have no health) are not Cancellable.
I think they should be.

@ryantheleach
Copy link
Contributor

How would you prevent an entity from dying when it already has 0 health? Wouldn't you rather listen for the entity damage event that would kill the entity and prevent damage?

import org.apache.commons.lang3.tuple.Pair;

/**
* A tuple of objects. This can be considered a {@link Pair}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then what's wrong with ImmutablePair?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That name works as well, however, using ImmutablePair still retains the method setValue which I don't want to ever include because of the simple fact that calling it equates to getting a giant UnsupportedOperationException.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I mean the ImmutablePair class in Guava.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I mean the ImmutablePair class in Guava.

Why can't I find it?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which is what I'm avoiding because of the comment I made three comments above this one.

@Faithcaio
Copy link
Contributor

I couldn't find an EntityDamageEvent.
There is one for Living but what I need is an event where entities without Health die/are removed.
(VehicleBreak and HangingBreakEvents in Bukkit)

@gabizou
Copy link
Member Author

gabizou commented Jun 8, 2015

I couldn't find an EntityDamageEvent.
There is one for Living but what I need is an event where entities without Health die/are removed.
(VehicleBreak and HangingBreakEvents in Bukkit)

https://github.com/SpongePowered/SpongeAPI/pull/712/files#diff-775b410055a6c5940497d25e4bcdff8dR43

@Faithcaio
Copy link
Contributor

So this one will be fired for non Living Entities too?

@gabizou
Copy link
Member Author

gabizou commented Jun 8, 2015

So this one will be fired for non Living Entities too?

That is the point of any EntityEvent, it is thrown for any Entity. The LivingChangeHealthEvent is specifically for any Living entities, etc.

@gabizou
Copy link
Member Author

gabizou commented Aug 6, 2015

After a lengthy discussion with @bloodmc and @modwizcode, I've come to the conclusion of the following agreements for this PR:

  • Causes will have two types of lists:
    • A list of Events for the event chain
    • A list of the Cause objects
    • Additional methods for querying said events in the Cause object chain:
      • getFirstEvent(Class<E extends Class>) returning the Event in the event chain
      • getLastEvent(Class<E extends Class>) returning the E event last in the event chain
      • getAllEventsOf(Class<E extends Class>) returning a List of all compatible events in the event chain
  • Each event that can have a custom direct cause would have extensive documentation of such causes so that querying for them in the Cause object is simple.
  • Custom event causes (such as SpawnCause or DamageReason) that include extra information as necessary related to the cause itself will remain as they are: included in the Cause list of objects.
  • Custom Cause interfaces would be superinterfaced for a simple getEventCauseUniqueId():UUID provided that they are caused (linked) to a previously called Event.
  • Events included in the event chain of Cause would become immutable by virtue of calling any modification methods resulting in an UsupportedOperationException. This will be possible through the event factory and an annotation provided by @Aaron1011.

"Do not go gentle into that good cause; Old reason should burn and rave
at close of event. Rage, rage against the dying of the cause."

Signed-off-by: Gabriel Harris-Rouquette <gabizou@me.com>
@gabizou
Copy link
Member Author

gabizou commented Aug 23, 2015

This has been merged into the refactor/event-names branch.

@gabizou gabizou closed this Aug 23, 2015
@gabizou gabizou deleted the feature/cause branch August 23, 2015 17:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants