Skip to content
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
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ ConditionalEvents runs actions when Minecraft events meet configured conditions:
commands, item use, joins, deaths, regions and more. Events can run once only or
on a cooldown. Once-only and cooldown data belong to the roleplay character in
play, so such events run only while a character is in play; `data_scope: account`
keeps an event's data per account instead. Events without that data run as
upstream. This copy tracks upstream 4.80.3, builds for
keeps an event's data per account instead, and `share_one_time: true` gives a
one-time event to every character of an account once one of them has done it,
running its `shared` action group. Events without that data run as upstream. This copy tracks upstream 4.80.3, builds for
Paper 1.21.10 and needs TLibs 2.6.1 with RPCharacters. It has no update checker or
bStats reporting.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public void onEnable(){
this.characterTracker = new CharacterTracker(playerManager, name -> {
CEEvent event = eventsManager.getEvent(name);
return event != null && event.usesCharacterData();
}, new LegacyRecipient(getDataFolder().getParentFile().toPath()), getLogger());
}, new LegacyRecipient(getDataFolder().getParentFile().toPath()), new OneTimeSharingManager(this), getLogger());

registerEvents();
registerCommands();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.tfminecraft.conditionalevents.character;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
Expand All @@ -27,19 +28,26 @@
* Which character each player is playing, readable from any thread, since events such as chat run
* off the main thread. On an account's first session it also gives the account's old data for
* per-character events to its most-played character, matching what Codex does with old journals.
* Shortly after a session starts, the character gets the shared one-time events (see
* {@link OneTimeSharing}) another of the account's characters has done.
*/
public final class CharacterTracker implements Listener {
/** Shared events arrive after the character's class and body are in place, and once the screen settles. */
static final long SHARE_DELAY_TICKS = 40L;

private final Map<UUID, UUID> current = new ConcurrentHashMap<>();
private final PlayerManager players;
private final Predicate<String> perCharacter;
private final LegacyRecipient recipients;
private final OneTimeSharing sharing;
private final Logger logger;

public CharacterTracker(PlayerManager players, Predicate<String> perCharacter, LegacyRecipient recipients,
Logger logger) {
OneTimeSharing sharing, Logger logger) {
this.players = players;
this.perCharacter = perCharacter;
this.recipients = recipients;
this.sharing = sharing;
this.logger = logger;
}

Expand All @@ -51,13 +59,13 @@ public Optional<UUID> character(UUID account) {
/** Picks up characters already in play when the plugin enables. Main thread only. */
public void start() {
for (Player player : Bukkit.getOnlinePlayers()) {
Characters.currentSession(player).ifPresent(session -> begin(session.ref()));
Characters.currentSession(player).ifPresent(session -> begin(player, session.ref()));
}
}

@EventHandler(priority = EventPriority.LOWEST)
public void onStart(CharacterSessionStartEvent event) {
begin(event.getSession().ref());
begin(event.getPlayer(), event.getSession().ref());
}

@EventHandler(priority = EventPriority.MONITOR)
Expand All @@ -71,9 +79,29 @@ public void onQuit(PlayerQuitEvent event) {
current.remove(event.getPlayer().getUniqueId());
}

private void begin(CharacterRef ref) {
private void begin(Player player, CharacterRef ref) {
seed(ref.ownerId());
current.put(ref.ownerId(), ref.characterId());
sharing.later(SHARE_DELAY_TICKS, () -> share(player, ref));
}

/**
* Marks the shared events another character of the account has done as done for this one and
* runs their {@code shared} action group, while that character is still in play.
*/
void share(Player player, CharacterRef ref) {
PlayerData data = players.getPlayerByUUID(ref.ownerId());
if (!player.isOnline() || data == null || !ref.characterId().equals(current.get(ref.ownerId()))) {
return;
}
List<String> shared = data.share(ref.characterId(), sharing.sharedEvents());
for (String event : shared) {
sharing.run(player, ref.characterId(), event, data.getResetEpoch(event));
}
if (!shared.isEmpty()) {
logger.info("Shared " + shared.size() + " one-time events of account " + ref.ownerId()
+ " with its character " + ref.characterId());
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package net.tfminecraft.conditionalevents.character;

import java.util.Collection;
import java.util.UUID;

import org.bukkit.entity.Player;

/**
* What sharing one-time events needs from the rest of the plugin. A per-character one-time event with
* {@code share_one_time: true} is done for every character of an account once one of them has done it;
* the others run its {@code shared} action group instead, for example to get its Player EXP.
*/
public interface OneTimeSharing {
/** The names of the events whose one-time flag is shared. */
Collection<String> sharedEvents();

/**
* Runs the event's {@code shared} action group for the player, if it has one, as a run of the
* character it was shared with, so the flag is undone if that character leaves before it ends.
*/
void run(Player player, UUID character, String event, long resetEpoch);

/** Runs the task on the main thread after the given number of ticks. */
void later(long ticks, Runnable task);
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public void configureEvents(){
List<ActionGroup> actionGroups = new ArrayList<ActionGroup>();
boolean oneTime = false;
boolean characterScoped = true;
boolean shareOneTime = false;
String ignoreWithPermission = null;
long cooldown = 0;
boolean enabled = true;
Expand Down Expand Up @@ -175,6 +176,9 @@ public void configureEvents(){
plugin.getLogger().warning("Event "+key+" has data_scope "+scope+"; use character or account. Using character.");
}
}
if(config.contains(path+".share_one_time")) {
shareOneTime = Boolean.valueOf(config.getString(path+".share_one_time"));
}
if(config.contains(path+".enabled")) {
enabled = Boolean.valueOf(config.getString(path+".enabled"));
}
Expand All @@ -199,6 +203,7 @@ public void configureEvents(){
event.setIgnoreWithPermission(ignoreWithPermission);
event.setOneTime(oneTime);
event.setCharacterScoped(characterScoped);
event.setShareOneTime(shareOneTime);
event.setEnabled(enabled);
event.setIgnoreIfCancelled(ignoreIfCancelled);
event.setPreventCooldownActivationActionGroups(preventCooldownActivationActionGroups);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package net.tfminecraft.conditionalevents.managers;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;

import org.bukkit.Bukkit;
import org.bukkit.entity.Player;

import net.tfminecraft.conditionalevents.ConditionalEvents;
import net.tfminecraft.conditionalevents.character.OneTimeSharing;
import net.tfminecraft.conditionalevents.model.CEEvent;
import net.tfminecraft.conditionalevents.model.internal.AdditionalEventStorage;
import net.tfminecraft.conditionalevents.model.internal.CharacterRun;
import net.tfminecraft.conditionalevents.model.internal.ExecutedEvent;

/** The plugin's side of shared one-time events: which events share, and their {@code shared} action group. */
public class OneTimeSharingManager implements OneTimeSharing {
public static final String SHARED_GROUP = "shared";

private final ConditionalEvents plugin;

public OneTimeSharingManager(ConditionalEvents plugin) {
this.plugin = plugin;
}

@Override
public Collection<String> sharedEvents() {
List<String> names = new ArrayList<>();
for (CEEvent event : plugin.getEventsManager().getEvents()) {
if (event.isEnabled() && event.sharesOneTime()) {
names.add(event.getName());
}
}
return names;
}

@Override
public void run(Player player, UUID character, String eventName, long resetEpoch) {
CEEvent event = plugin.getEventsManager().getEvent(eventName);
if (event == null || event.getActionGroup(SHARED_GROUP) == null) {
return;
}
CharacterRun run = new CharacterRun(character);
run.markedOneTime(resetEpoch);
ExecutedEvent executed = new ExecutedEvent(player, new ArrayList<>(), event, SHARED_GROUP, null, null, plugin,
new AdditionalEventStorage());
executed.bindToCharacter(run);
plugin.getDebugManager().sendActionsMessage(eventName, SHARED_GROUP, player);
executed.executeActions();
}

@Override
public void later(long ticks, Runnable task) {
Bukkit.getScheduler().runTaskLater(plugin, task, ticks);
}
}
13 changes: 13 additions & 0 deletions src/main/java/net/tfminecraft/conditionalevents/model/CEEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class CEEvent {

private boolean oneTime;
private boolean characterScoped = true;
private boolean shareOneTime;

private String ignoreWithPermission;

Expand Down Expand Up @@ -92,6 +93,18 @@ public void setCharacterScoped(boolean characterScoped) {
this.characterScoped = characterScoped;
}

/**
* Whether a character gets this per-character one-time event once any other character of its
* account has done it ({@code share_one_time: true}), running its {@code shared} action group.
*/
public boolean sharesOneTime() {
return shareOneTime && oneTime && characterScoped;
}

public void setShareOneTime(boolean shareOneTime) {
this.shareOneTime = shareOneTime;
}

public void setOneTime(boolean oneTime) {
this.oneTime = oneTime;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package net.tfminecraft.conditionalevents.model.player;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
Expand Down Expand Up @@ -110,6 +111,26 @@ public synchronized int seed(UUID character, Predicate<String> events) {
return moved;
}

/**
* Marks done for the character each of the events that another of the account's characters has
* done and it has not.
* @return the events marked, in the order given
*/
public synchronized List<String> share(UUID character, Collection<String> events) {
List<String> shared = new ArrayList<>();
for (String event : events) {
if (!isEventOneTime(character, event) && characterEventData.keySet().stream()
.anyMatch(other -> !other.equals(character) && isEventOneTime(other, event))) {
shared.add(event);
}
}
// Marked after the search: the character's first mark adds it to the map being searched.
for (String event : shared) {
setOneTime(character, event, true);
}
return shared;
}

public synchronized Snapshot snapshot() {
Map<UUID, List<EventData>> characters = new HashMap<>();
characterEventData.forEach((character, events) -> characters.put(character, copy(events)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,39 @@
private PlayerManager players;
private CharacterTracker tracker;
private Player player;
private FakeSharing sharing;

private record Run(UUID character, String event, long epoch) {
}

private static final class FakeSharing implements OneTimeSharing {
List<String> events = List.of();
final List<Long> delays = new ArrayList<>();
final List<Runnable> tasks = new ArrayList<>();
final List<Run> runs = new ArrayList<>();

@Override
public java.util.Collection<String> sharedEvents() {
return events;
}

@Override
public void run(Player player, UUID character, String event, long resetEpoch) {
runs.add(new Run(character, event, resetEpoch));
}

@Override
public void later(long ticks, Runnable task) {
delays.add(ticks);
tasks.add(task);
}

void runAll() {
List<Runnable> due = new ArrayList<>(tasks);
tasks.clear();
due.forEach(Runnable::run);
}
}

@BeforeEach
void setUp() {
Expand All @@ -72,7 +105,9 @@
public void close() {
}
});
tracker = new CharacterTracker(players, Set.of("research_bronze")::contains, new LegacyRecipient(plugins), logger);
sharing = new FakeSharing();
tracker = new CharacterTracker(players, Set.of("research_bronze")::contains, new LegacyRecipient(plugins), sharing,
logger);
player = mock(Player.class);
when(player.getUniqueId()).thenReturn(owner);
}
Expand Down Expand Up @@ -100,7 +135,7 @@
void quittingForgetsTheCharacter() {
tracker.onStart(start(alt));

tracker.onQuit(new PlayerQuitEvent(player, "bye"));

Check warning on line 138 in src/test/java/net/tfminecraft/conditionalevents/character/CharacterTrackerTest.java

View workflow job for this annotation

GitHub Actions / build

PlayerQuitEvent(org.bukkit.entity.Player,java.lang.String) in org.bukkit.event.player.PlayerQuitEvent has been deprecated and marked for removal

assertTrue(tracker.character(owner).isEmpty());
}
Expand Down Expand Up @@ -173,6 +208,60 @@
assertTrue(logs.isEmpty());
}

@Test
void aCharacterGetsTheSharedEventsItsAccountHasDone() {
sharing.events = List.of("duchy_1", "duchy_2", "duchy_3");
when(player.isOnline()).thenReturn(true);
PlayerData data = new PlayerData(owner, "Steve");
data.setSeededTo(other);
data.setOneTime(other, "duchy_1", true);
data.setOneTime(other, "duchy_2", true);
data.setOneTime(alt, "duchy_2", true);
data.setOneTime("duchy_3", true);
data.resetEvent("duchy_1");
data.setOneTime(other, "duchy_1", true);
players.addPlayer(data);

tracker.onStart(start(alt));
assertEquals(List.of(CharacterTracker.SHARE_DELAY_TICKS), sharing.delays);
assertFalse(data.isEventOneTime(alt, "duchy_1"), "Nothing arrives before the delay");
sharing.runAll();

assertTrue(data.isEventOneTime(alt, "duchy_1"));
assertFalse(data.isEventOneTime(alt, "duchy_3"), "Account-level data is not a character's");
assertEquals(List.of(new Run(alt, "duchy_1", 1)), sharing.runs, "Only what the character lacked runs");
assertTrue(logs.stream().anyMatch(r -> r.getMessage().contains(
"Shared 1 one-time events of account " + owner + " with its character " + alt)));

tracker.onStart(start(alt));
sharing.runAll();
assertEquals(1, sharing.runs.size(), "Nothing is shared twice");
}

@Test
void sharingWaitsForTheSameCharacterToStillBeInPlay() {
sharing.events = List.of("duchy_1");
PlayerData data = new PlayerData(owner, "Steve");
data.setSeededTo(other);
data.setOneTime(other, "duchy_1", true);

tracker.onStart(start(alt));
sharing.runAll();
players.addPlayer(data);
tracker.onStart(start(alt));
sharing.runAll();
when(player.isOnline()).thenReturn(true);
tracker.onStart(start(alt));
tracker.onEnd(end(alt));
sharing.runAll();
tracker.onStart(start(alt));
tracker.onStart(start(other));
sharing.runAll();

assertFalse(data.isEventOneTime(alt, "duchy_1"));
assertTrue(sharing.runs.isEmpty(), "No player data, an offline player or a character no longer in play get nothing");
}

private CharacterInfo played(UUID character, long seconds) {
return new CharacterInfo(new CharacterRef(owner, character), "Name", CharacterStatus.ALIVE, null, Set.of(), seconds, null);
}
Expand Down
Loading
Loading