이벤트 원인
이벤트는 게임 내에서 어떤 사건이나 행동에 대한 논리적인 변화를 주는 데 유용합니다. 하지만 이벤트 자체에는 무엇이 그 이벤트를 **발생**시켰느냐에 대한 문맥을 거의 제공하지 못한다는 문제점이 있습니다. Cause 객체는 이벤트에 대한 부가적인 문맥적 정보를 제공하고 받는 역할을 합니다.
For example, a world protection plugin needs information on what player has caused a ChangeBlockEvent to
occur before they can decide if the event should be cancelled or not. Rather than go with the traditional route of
creating a multitude of subevents for the different source conditions this information is instead provided in the
Cause
of the event.
Every event provides a Cause
object which can be interrogated for the information pertaining to why the event was
fired. The Cause object can be retrieved from an event by simply calling Event#cause().
Cause and Context
The Cause
object contains two distinct sets of information, the cause stack and the EventContext.
The cause stack of the event are the direct causes captured in order of importance. There are no names attached to the objects in the cause stack.
The event context contains extra information surrounding the event. Contexts are attached to keys but have no order, they are all equally important.
As an example, if a sheep owned by a player eats some grass, the most direct cause of this is the sheep. The
player would be in the EventContext
as the EventContextKeys#PLAYER, giving event consumers
that additional information about how the event has come about, but would not necessarily be within the
direct cause itself.
Another example that you may need to watch out for is if you simulate a player. The simulated player may not be
in the direct cause, as the player being simulated may not have been involved in the action, however, the player
would be in the EventContext
under the EventContextKeys#PLAYER_SIMULATED
Retrieving Objects from the Direct Cause
구조적으로, Cause
객체는 객체들의 순차적인 배열을 갖고 있습니다. 아래에서 설명하겠지만, Cause에서 정보를 가져오는 데 사용하는 메소드는 여러 가지가 있습니다. 전체 메소드 목록을 확인하려면 Javadoc 을 확인하세요.
참고
Cause 내부의 객체들은 순차적으로 정렬되어 있습니다. 첫 번째 객체는 이벤트에 가장 직접적인 원인이고, 그 다음 객체들은 중요도가 낮아지고(또는) 문맥적 정보만 제공하는 것입니다.
Cause#root() 는 Cause 객체의 첫 번째 객체를 반환합니다. 이 객체는 이벤트를 발생시키는 데 가장 가깝고 직접적인 원인입니다. Cause
는 비어 있을 수 없으므로, root
객체는 항상 받을 수 있습니다.
Cause#first(Class) returns the first object in the cause chain whose type is either the same as or is a
subtype of the given class. For example, given a cause which contained a player followed by an entity
[Player, Entity, ...]
@Listener
public void onEvent(ExampleCauseEvent event) {
Cause cause = event.cause(); // [Player, Entity]
Optional<Player> firstPlayer = cause.first(Player.class); // 1
Optional<Entity> firstEntity = cause.first(Entity.class); // 2
}
Both optionals would contain the player object as its type directly matched request for a Player type and it matched the request for an Entity type as Player is a subtype of Entity.
Cause#last(Class) 는 Cause#first(Class)
와 동일한 조건에서 가장 마지막에 오는 값을 반환합니다.
위 예제 코드에서, Cause#last(Class)
를 대신 사용했다면 firstPlayer는 여전히 Player 객체를 갖겠지만, firstEntity는 Cause 객체 사슬의 두 번째 원소인 Entity를 가지게 될 것입니다.
Cause#containsType(Class) 는 Cause 객체 사슬에서 주어진 클래스(타입)와 일치하는 객체의 존재 여부를 boolean 값으로 반환합니다.
Cause#all() 은 Cause 객체 사슬에 있는 모든 객체를 List 배열로 반환합니다.
Event Context
Sometimes the ordering of objects within the cause isn’t enough to get the proper idea of what an object represents in
relation to the event. This is where EventContext comes in. The event context allows objects to be
associated with unique names, in the form of EventContextKeys, allowing them to be easily identified and
requested. Some examples of use cases for named causes is the Notifier of a ChangeBlockEvent.All or the
Source
of a DamageEntityEvent.
Unlike the cause stack, which makes no guarantees as to the objects contained witin it, an object associated with a EventContextKey is guaranteed to be of the type specified by the key.
Retrieving a entry from the context of a cause
@Listener
public void onGrow(ChangeBlockEvent.All event) {
Optional<UUID> notifier = event.cause().context().get(EventContextKeys.NOTIFIER);
}
This example makes use of EventContext#get(EventContextKey) which can be used to retrieve the expected object
associated with a name if it is present within the context. Additionally EventContext#asMap() provides
a Map<EventContextKey<?>, Object>
which can be used to find all present EventContextKey
s and their associated
objects.
참고
Some common identifying names for EventContextKey
s are present as static fields in the
EventContextKeys
class.
Creating Custom Causes
Creating a cause is easy, but depends on whether you are creating your cause on the main server thread or async.
참고
Cause objects are immutable therefore cannot be modified once created.
Using the CauseStackManager
경고
The CauseStackManager
only works on the main server thread. If you call it from a
different thread, an IllegalStateException
will be thrown. Ensure you are on the main
server thread before calling methods on the CauseStackManager
.
If you are creating your event on the main thread, then use the CauseStackManager from the
appropriate Engine, which can be found at Engine#causeStackManager().
The CauseStackManager
tracks the potential causes of events as the game runs, allowing for easy
retrieval of the current Cause
without effort. To see the current cause, call
CauseStackManager#currentCause(). You may notice that your plugin’s PluginContainer
is already in the returned Cause
, as plugins are one of the objects tracked by the manager. Using the
CauseStackManager
for creating causes removes the need for boilerplate-like code where you supply
objects like your plugin container, so that you can concentrate on adding your own causes.
Before adding your own causes, you should push a cause stack frame to the manager. Adding a frame acts as a saved state, when you are done with your causes, the removal of the frame returns the manager to its original state.
팁
Adding a frame to the CauseStackManager does not remove what is already in the manager, so anything
that is in the cause stack and contexts before a stack frame is added will be there afterwards. You
can verify this by calling Sponge.server().causeStackManager().currentCause()
before and after the
frame is pushed.
For example, if the cause stack contains a PluginContainer
and a ServerPlayer
when a frame
is pushed, they will remain on the stack and will form part of the Cause
if one is obtained from
the frame.
For example, if you were to fire an event that was simulating another player in a sudo like command,
you may want to add the player you are acting as in the cause and the GameProfile
of the player that you are
simulating in the context (as the simulated player is not directly responsible for the event being fired.)
Creating a custom Cause with the CauseStackManager
In this example, the variables would be populated, the cause would contain the playerToSimulate
as
the root cause, the sourceRunningSudo
as the second object in the cause and the GameProfile
as the EventContextKeys#PLAYER_SIMULATED context, in addition to anything already in the
CauseStackManager
. Your event code would be at the bottom of the method.
CommandSource sourceRunningSudo = ...;
Player playerToSimulate = ...;
try (CauseStackManager.StackFrame frame = Sponge.server().causeStackManager().pushCauseFrame()) {
frame.pushCause(sourceRunningSudo);
frame.pushCause(playerToSimulate);
frame.addContext(EventContextKeys.PLAYER_SIMULATED, playerToSimulate.getProfile());
Cause cause = frame.currentCause();
}
Note that the last item you push to the cause stack will be the root of the Cause
as
stacks are “last in, first out” (LIFO) structures.
팁
For more information about the stack data type and why the order matters, see the Stack javadocs or this Wikipedia article.
Using the Cause Builder
If you are creating an event that does not fire on the main thread, you cannot use the
CauseStackManager
. Instead, you will need to create a Cause
object manually.
Creating a cause object is easy using the Cause.Builder. You can obtain a
builder by calling Cause.builder()
. To add a cause to the builder, use the
Cause.Builder#append(Object) method, but note that unlike the CauseStackManager
,
the first element you add will be the root, not the last.
If you wish to add contexts, there is a separate builder for those, the
EventContext.Builder, accessed by calling EventContext#builder()
.
The EventContext
can then be added using the Cause.Builder#build(EventContext)
when
you have finished building the Cause
up.
Taking the previous example, this is how we would build it using the cause builder.
Creating a custom Cause with the Cause and EventContext builders
Note that in this example, the variables would be populated, and that the first entry appended to the cause would be the root cause.
CommandSource sourceRunningSudo = ...;
Player playerToSimulate = ...;
PluginContainer plugin = ...;
EventContext context = EventContext.builder()
.add(EventContextKeys.PLAYER_SIMULATED, playerToSimulate.profile())
.add(EventContextKeys.PLUGIN, plugin)
.build();
Cause cause = Cause.builder()
.append(playerToSimulate)
.append(sourceRunningSudo)
.append(plugin)
.build(context);
Think carefully about what information to include in your cause.
If you’re firing an event from your plugin which is usually triggered through other means,
it is a good idea to include your PluginContainer
in the cause so other plugins know
that the event comes from your plugin. If you are firing the event on behalf of a player
due to some action it’s usually a good idea to include that player in the cause.