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

feat: use custom event handling over external dependency #112

Merged
merged 2 commits into from
Aug 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar

dependencies {
api(libs.event)
implementation(libs.gson)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@

package com.github.juliarn.npclib.api.event;

import com.seiama.event.Cancellable;
public interface CancellableNpcEvent extends NpcEvent {

public interface CancellableNpcEvent extends Cancellable {
boolean cancelled();

void cancelled(boolean cancelled);
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,93 +24,117 @@

package com.github.juliarn.npclib.api.event.manager;

import com.github.juliarn.npclib.api.event.CancellableNpcEvent;
import com.github.juliarn.npclib.api.event.NpcEvent;
import com.github.juliarn.npclib.api.log.PlatformLogger;
import com.seiama.event.EventConfig;
import com.seiama.event.EventSubscription;
import com.seiama.event.bus.EventBus;
import com.seiama.event.bus.SimpleEventBus;
import com.seiama.event.registry.EventRegistry;
import com.seiama.event.registry.SimpleEventRegistry;
import java.util.OptionalInt;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Predicate;
import org.jetbrains.annotations.NotNull;

final class DefaultNpcEventManager implements NpcEventManager {

private final EventBus<NpcEvent> eventBus;
private final EventRegistry<NpcEvent> eventRegistry;
private static final Comparator<NpcEventSubscription<? super NpcEvent>> SUBSCRIPTION_COMPARABLE =
Comparator.comparingInt(NpcEventSubscription::order);

private final boolean debugEnabled;
private final PlatformLogger platformLogger;

private final Map<Class<?>, List<NpcEventSubscription<? super NpcEvent>>> registeredSubscribers =
new ConcurrentHashMap<>(16, 0.9f, 1);

public DefaultNpcEventManager(boolean debugEnabled, @NotNull PlatformLogger logger) {
this.eventRegistry = new SimpleEventRegistry<>(NpcEvent.class);
this.eventBus = new SimpleEventBus<>(this.eventRegistry, new LoggingEventExceptionHandler(debugEnabled, logger));
this.debugEnabled = debugEnabled;
this.platformLogger = logger;
}

private static boolean isEventCancelled(@NotNull NpcEvent event) {
return event instanceof CancellableNpcEvent && ((CancellableNpcEvent) event).cancelled();
}

@Override
public <E extends NpcEvent> @NotNull E post(@NotNull E event) {
this.eventBus.post(event, OptionalInt.empty());
Objects.requireNonNull(event, "event");

for (Map.Entry<Class<?>, List<NpcEventSubscription<? super NpcEvent>>> entry : this.registeredSubscribers.entrySet()) {
Class<?> subscribedEventType = entry.getKey();
List<NpcEventSubscription<? super NpcEvent>> subscriptions = entry.getValue();

if (subscribedEventType.isInstance(event) && !subscriptions.isEmpty()) {
for (NpcEventSubscription<? super E> subscription : subscriptions) {
// once the event was cancelled we don't want to post it to any further subscribers
boolean eventWasCancelled = isEventCancelled(event);
if (eventWasCancelled) {
break;
}

try {
subscription.eventConsumer().handle(event);
} catch (Throwable throwable) {
EventExceptionHandler.rethrowFatalException(throwable);
if (this.debugEnabled) {
// not a fatal exception but debug is enabled to we log it anyway
this.platformLogger.error(
String.format(
"Subscriber %s was unable to handle %s",
subscription.eventConsumer().getClass().getName(),
event.getClass().getSimpleName()),
throwable);
}
}
}
}
}

return event;
}

@Override
public <E extends NpcEvent> @NotNull NpcEventManager registerEventHandler(
public <E extends NpcEvent> @NotNull NpcEventSubscription<? super E> registerEventHandler(
@NotNull Class<E> eventType,
@NotNull NpcEventConsumer<E> consumer
) {
return this.registerEventHandler(eventType, consumer, EventConfig.DEFAULT_ORDER);
return this.registerEventHandler(eventType, consumer, 0);
}

@Override
public <E extends NpcEvent> @NotNull NpcEventManager registerEventHandler(
@SuppressWarnings("unchecked")
public <E extends NpcEvent> @NotNull NpcEventSubscription<? super E> registerEventHandler(
@NotNull Class<E> eventType,
@NotNull NpcEventConsumer<E> consumer,
int eventHandlerPriority
) {
EventConfig eventConfig = EventConfig.defaults().acceptsCancelled(false).order(eventHandlerPriority);
this.eventRegistry.subscribe(eventType, eventConfig, consumer::accept);
return this;
}

private static final class LoggingEventExceptionHandler implements EventBus.EventExceptionHandler {
NpcEventSubscription<? super E> subscription = new DefaultNpcEventSubscription<>(
eventHandlerPriority,
eventType,
consumer,
this);

private final boolean debugEnabled;
private final PlatformLogger logger;
List<NpcEventSubscription<? super NpcEvent>> eventSubscriptions = this.registeredSubscribers.computeIfAbsent(
eventType,
__ -> new CopyOnWriteArrayList<>());
eventSubscriptions.add((NpcEventSubscription<? super NpcEvent>) subscription);

public LoggingEventExceptionHandler(boolean debugEnabled, @NotNull PlatformLogger logger) {
this.debugEnabled = debugEnabled;
this.logger = logger;
}

private static boolean isFatal(@NotNull Throwable throwable) {
// this includes the most fatal errors that can occur on a thread which we should not silently ignore and rethrow
return throwable instanceof InterruptedException
|| throwable instanceof LinkageError
|| throwable instanceof ThreadDeath
|| throwable instanceof VirtualMachineError;
}
eventSubscriptions.sort(SUBSCRIPTION_COMPARABLE);
return subscription;
}

@SuppressWarnings("unchecked")
private static <T extends Throwable> void throwUnchecked(@NotNull Throwable throwable) throws T {
throw (T) throwable;
@Override
public void unregisterEventHandlerIf(@NotNull Predicate<NpcEventSubscription<? super NpcEvent>> subscriptionFilter) {
for (List<NpcEventSubscription<? super NpcEvent>> subscriptions : this.registeredSubscribers.values()) {
subscriptions.removeIf(subscriptionFilter);
}
}

@Override
public <E> void eventExceptionCaught(
@NotNull EventSubscription<? super E> subscription,
@NotNull E event,
@NotNull Throwable throwable
) {
if (isFatal(throwable)) {
// rethrow fatal exceptions instantly
throwUnchecked(throwable);
} else if (this.debugEnabled) {
// just log that we received an exception from the event handler
this.logger.error(
String.format(
"Subscriber %s was unable to handle %s:",
subscription.subscriber().getClass().getName(),
event.getClass().getSimpleName()),
throwable);
}
void removeSubscription(@NotNull NpcEventSubscription<?> subscription) {
List<NpcEventSubscription<? super NpcEvent>> subscriptions = this.registeredSubscribers.get(
subscription.eventType());
if (subscriptions != null) {
subscriptions.remove(subscription);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* This file is part of npc-lib, licensed under the MIT License (MIT).
*
* Copyright (c) 2022-2023 Julian M., Pasqual K. and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package com.github.juliarn.npclib.api.event.manager;

import com.github.juliarn.npclib.api.event.NpcEvent;
import org.jetbrains.annotations.NotNull;

final class DefaultNpcEventSubscription<E extends NpcEvent> implements NpcEventSubscription<E> {

private final int order;
private final Class<E> eventType;
private final NpcEventConsumer<E> consumer;

private final DefaultNpcEventManager eventManager;

public DefaultNpcEventSubscription(
int order,
@NotNull Class<E> eventType,
@NotNull NpcEventConsumer<E> consumer,
@NotNull DefaultNpcEventManager eventManager
) {
this.order = order;
this.eventType = eventType;
this.consumer = consumer;
this.eventManager = eventManager;
}

@Override
public int order() {
return this.order;
}

@Override
public @NotNull Class<E> eventType() {
return this.eventType;
}

@Override
public @NotNull NpcEventConsumer<E> eventConsumer() {
return this.consumer;
}

@Override
public void dispose() {
this.eventManager.removeSubscription(this);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* This file is part of npc-lib, licensed under the MIT License (MIT).
*
* Copyright (c) 2022-2023 Julian M., Pasqual K. and contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

package com.github.juliarn.npclib.api.event.manager;

import org.jetbrains.annotations.NotNull;

final class EventExceptionHandler {

private EventExceptionHandler() {
throw new UnsupportedOperationException();
}

public static void rethrowFatalException(@NotNull Throwable throwable) {
if (isFatal(throwable)) {
throwUnchecked(throwable);
}
}

private static boolean isFatal(@NotNull Throwable throwable) {
// this includes the most fatal errors that can occur on a thread which we should not silently ignore and rethrow
return throwable instanceof InterruptedException
|| throwable instanceof LinkageError
|| throwable instanceof ThreadDeath
|| throwable instanceof VirtualMachineError;
}

@SuppressWarnings("unchecked")
private static <T extends Throwable> void throwUnchecked(@NotNull Throwable throwable) throws T {
throw (T) throwable;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@
package com.github.juliarn.npclib.api.event.manager;

import com.github.juliarn.npclib.api.event.NpcEvent;
import java.util.function.Consumer;
import org.jetbrains.annotations.NotNull;

@FunctionalInterface
public interface NpcEventConsumer<E extends NpcEvent> extends Consumer<E> {
public interface NpcEventConsumer<E extends NpcEvent> {

void handle(@NotNull E event) throws Exception;
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import com.github.juliarn.npclib.api.event.NpcEvent;
import com.github.juliarn.npclib.api.log.PlatformLogger;
import java.util.Objects;
import java.util.function.Predicate;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;

Expand All @@ -38,14 +39,17 @@ public interface NpcEventManager {
return new DefaultNpcEventManager(debugEnabled, logger);
}

@Contract("_ -> param1")
<E extends NpcEvent> @NotNull E post(@NotNull E event);

<E extends NpcEvent> @NotNull NpcEventManager registerEventHandler(
<E extends NpcEvent> @NotNull NpcEventSubscription<? super E> registerEventHandler(
@NotNull Class<E> eventType,
@NotNull NpcEventConsumer<E> consumer);

<E extends NpcEvent> @NotNull NpcEventManager registerEventHandler(
<E extends NpcEvent> @NotNull NpcEventSubscription<? super E> registerEventHandler(
@NotNull Class<E> eventType,
@NotNull NpcEventConsumer<E> consumer,
int eventHandlerPriority);

void unregisterEventHandlerIf(@NotNull Predicate<NpcEventSubscription<? super NpcEvent>> subscriptionFilter);
}
Loading