Skip to content

Commit

Permalink
feat: start event system
Browse files Browse the repository at this point in the history
  • Loading branch information
qixils committed Sep 24, 2024
1 parent 9fa3195 commit 0c6761f
Show file tree
Hide file tree
Showing 11 changed files with 407 additions and 59 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies {
implementation(libs.jackson.databind)
implementation(libs.websocket)
implementation(libs.jwt)
api(libs.geantyref)
api(libs.annotations)
}

Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ gson = "2.11.0"
jackson = "2.17.2"
annotations = "24.0.0"
jwt = "4.4.0"
geantyref = "2.0.0"

[libraries]
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
Expand All @@ -18,3 +19,4 @@ jackson-annotations = { module = "com.fasterxml.jackson.core:jackson-annotations
jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref="jackson" }
annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" }
jwt = { module = "com.auth0:java-jwt", version.ref = "jwt" }
geantyref = { module = "io.leangen.geantyref:geantyref", version.ref = "geantyref" }
21 changes: 10 additions & 11 deletions src/main/java/live/crowdcontrol/cc4j/CCEffect.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import live.crowdcontrol.cc4j.websocket.data.CCEffectResult;
import live.crowdcontrol.cc4j.websocket.payload.PublicEffectPayload;
import org.intellij.lang.annotations.Pattern;
import org.intellij.lang.annotations.RegExp;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand All @@ -12,16 +11,16 @@
public interface CCEffect {

@RegExp
String EFFECT_ID_PATTERN = "^[a-zA-Z_][a-zA-Z0-9_]*$";
String EFFECT_ID_PATTERN = "^(?!__cc)[a-zA-Z_][a-zA-Z0-9_]*$";

/**
* Gets the ID of the effect.
* Used to determine which effect object to execute.
*
* @return effect ID
*/
@Pattern(EFFECT_ID_PATTERN)
String effectID();
// /**
// * Gets the ID of the effect.
// * Used to determine which effect object to execute.
// *
// * @return effect ID
// */
// @Pattern(EFFECT_ID_PATTERN)
// String effectID();

// TODO: reference const in javadoc
/**
Expand All @@ -36,5 +35,5 @@ public interface CCEffect {
* @return the result, if available
*/
@Nullable
CCEffectResult onTriggerEffect(@NotNull PublicEffectPayload request, @NotNull CCPlayer source);
CCEffectResult onTrigger(@NotNull PublicEffectPayload request, @NotNull CCPlayer source);
}
81 changes: 81 additions & 0 deletions src/main/java/live/crowdcontrol/cc4j/CCEventType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package live.crowdcontrol.cc4j;

import io.leangen.geantyref.TypeToken;
import live.crowdcontrol.cc4j.websocket.payload.PublicEffectPayload;
import org.jetbrains.annotations.NotNull;

import java.util.Objects;

public class CCEventType<T> {
private final @NotNull String listenerId;
private final @NotNull TypeToken<T> typeToken;

public CCEventType(@NotNull String listenerId, @NotNull TypeToken<T> typeToken) {
this.listenerId = listenerId;
this.typeToken = typeToken;
}

public CCEventType(@NotNull String listenerId, @NotNull Class<T> clazz) {
this(listenerId, TypeToken.get(clazz));
}

public static CCEventType<Void> ofVoid(@NotNull String listenerId) {
return new CCEventType<>(listenerId, Void.class);
}

public @NotNull String getListenerId() {
return listenerId;
}

public @NotNull TypeToken<T> getTypeToken() {
return typeToken;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CCEventType<?> that = (CCEventType<?>) o;
return Objects.equals(listenerId, that.listenerId) && Objects.equals(typeToken, that.typeToken);
}

@Override
public int hashCode() {
return Objects.hash(listenerId, typeToken);
}

/**
* Called when a player's WebSocket initially establishes its connection.
* The connectionID will be unavailable at this point.
*/
public static final CCEventType<Void> CONNECTION = ofVoid("connection");

/**
* Called when a player's WebSocket connectionID becomes known.
*/
public static final CCEventType<Void> IDENTIFIED = ofVoid("identified");

/**
* Called when a player's WebSocket becomes authenticated.
* The connectionID may be unavailable at this point.
*/
public static final CCEventType<Void> AUTHENTICATED = ofVoid("authenticated");

/**
* Called when a player's authentication token expires.
* This may happen if they have not re-authenticated in about 6 months.
*/
public static final CCEventType<Void> AUTH_EXPIRED = ofVoid("auth_expired");

/**
* Called when a player's auth token is removed for any reason.
* Shares overlap with {@link #AUTH_EXPIRED}.
*/
public static final CCEventType<Void> UNAUTHENTICATED = ofVoid("unauthenticated");

/**
* Called when an {@code effect-request} on the {@code pub} domain comes in from the player.
* Note that the traditional way to receive this information is via {@link CCEffect#onTrigger(PublicEffectPayload, CCPlayer)}.
*/
public static final CCEventType<PublicEffectPayload> PUB_EFFECT_REQUEST = new CCEventType<>("pub_effect_request", PublicEffectPayload.class);
}
7 changes: 7 additions & 0 deletions src/main/java/live/crowdcontrol/cc4j/CCPlayer.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package live.crowdcontrol.cc4j;

import live.crowdcontrol.cc4j.util.EventManager;
import live.crowdcontrol.cc4j.websocket.UserToken;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand Down Expand Up @@ -40,4 +41,10 @@ public interface CCPlayer {
*/
@Nullable
UserToken getUserToken();

/**
* Gets the manager which handles distributing events.
*/
@NotNull
EventManager getEventManager();
}
10 changes: 10 additions & 0 deletions src/main/java/live/crowdcontrol/cc4j/CCTimedEffect.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
package live.crowdcontrol.cc4j;

import live.crowdcontrol.cc4j.websocket.payload.PublicEffectPayload;
import org.jetbrains.annotations.NotNull;

public interface CCTimedEffect extends CCEffect {

/**
* Pauses this effect.
*/
void onPause(@NotNull PublicEffectPayload request, @NotNull CCPlayer source);

/**
* Resumes this effect.
*/
}
100 changes: 85 additions & 15 deletions src/main/java/live/crowdcontrol/cc4j/CrowdControl.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,45 +14,115 @@
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;

import static live.crowdcontrol.cc4j.CCEffect.EFFECT_ID_PATTERN;

public class CrowdControl {
private static final Logger log = LoggerFactory.getLogger(CrowdControl.class);
protected final Map<String, CCEffect> effects = new HashMap<>();
protected final Map<String, Supplier<CCEffect>> effects = new HashMap<>();
protected final Map<UUID, ConnectedPlayer> players = new HashMap<>();
protected final ExecutorService effectPool = Executors.newCachedThreadPool();
protected final ScheduledExecutorService timedEffectPool = Executors.newScheduledThreadPool(20);
protected final ExecutorService eventPool = Executors.newCachedThreadPool();
protected final Path dataFolder;
protected AtomicInteger runningEffects;

public CrowdControl(@NotNull Path dataFolder) {
this.dataFolder = dataFolder;
}

/**
* Gets the folder in which players' Crowd Control tokens are stored.
*
* @return data folder
*/
public Path getDataFolder() {
return dataFolder;
}

/**
* Gets the executor service on which effects are to be run.
*
* @return executor service
*/
public @NotNull ExecutorService getEffectPool() {
return effectPool;
}

/**
* Gets the executor service on which timed effect updates are to be run.
*
* @return scheduled executor service
*/
public @NotNull ScheduledExecutorService getTimedEffectPool() {
return timedEffectPool;
}

/**
* Gets the executor service on which events are to be run.
*
* @return executor service
*/
public @NotNull ExecutorService getEventPool() {
return eventPool;
}

@Nullable
public CCPlayer getPlayer(@NotNull UUID playerID) {
ConnectedPlayer existing = players.get(playerID);
public CCPlayer getPlayer(@NotNull UUID playerId) {
ConnectedPlayer existing = players.get(playerId);
if (existing == null) return null;
if (!existing.isOpen()) {
players.remove(playerID);
players.remove(playerId);
return null;
}
return existing;
}

@NotNull
public CCPlayer addPlayer(@NotNull UUID playerID) {
CCPlayer existing = getPlayer(playerID);
public CCPlayer addPlayer(@NotNull UUID playerId) {
CCPlayer existing = getPlayer(playerId);
if (existing != null) {
log.warn("Asked to add player {} with existing connection", playerID);
log.warn("Asked to add player {} with existing connection", playerId);
return existing;
}
ConnectedPlayer player = new ConnectedPlayer(playerID, dataFolder);
ConnectedPlayer player = new ConnectedPlayer(playerId, this);
player.connect();
players.put(playerID, player);
players.put(playerId, player);
return player;
}

public boolean addEffect(@NotNull CCEffect effect) {
String effectID = effect.effectID();
public boolean removePlayer(@NotNull UUID playerId) {
ConnectedPlayer existing = players.remove(playerId);
if (existing == null) return false;
if (existing.isOpen())
existing.close();
return true;
}

/**
* Registers an effect which maintains one object across its lifetime.
*
* @param effectID ID of the effect
* @param effect executor object
* @return whether the effect was added successfully
*/
public boolean addEffect(@NotNull String effectID, @NotNull CCEffect effect) {
return addEffect(effectID, () -> effect);
}

/**
* Registers an effect which is instantiated upon triggering.
*
* @param effectID ID of the effect
* @param supplier executor supplier
* @return whether the effect was added successfully
*/
public boolean addEffect(@NotNull String effectID, @NotNull Supplier<@NotNull CCEffect> supplier) {
if (!effectID.matches(EFFECT_ID_PATTERN)) {
log.error("Effect ID {} should match pattern {}", effectID, EFFECT_ID_PATTERN);
return false;
Expand All @@ -61,15 +131,15 @@ public boolean addEffect(@NotNull CCEffect effect) {
log.error("Effect ID {} is already registered", effectID);
return false;
}
effects.put(effectID, effect);
effects.put(effectID, supplier);
return true;
}

@Nullable
public CCEffectResult executeEffect(@NotNull PublicEffectPayload payload, @NotNull ConnectedPlayer source) {
String effectID = payload.getEffect().getEffectID();
CCEffect effect = effects.get(effectID);
if (effect == null) {
Supplier<CCEffect> supplier = effects.get(effectID);
if (supplier == null) {
log.error("Cannot execute unknown effect {}", effectID);
return new CCInstantEffectResult(
payload.getRequestID(),
Expand All @@ -78,7 +148,7 @@ public CCEffectResult executeEffect(@NotNull PublicEffectPayload payload, @NotNu
);
}
try {
return effect.onTriggerEffect(payload, source);
return supplier.get().onTrigger(payload, source);
} catch (Exception e) {
log.error("Failed to invoke effect {}", effectID, e);
return new CCInstantEffectResult(
Expand Down
Loading

0 comments on commit 0c6761f

Please sign in to comment.