Skip to content

Commit 117483d

Browse files
Add arena revocation tracker and rejoin-enforcing encounter manager
1 parent 562a893 commit 117483d

3 files changed

Lines changed: 207 additions & 0 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.github.legendaryforge.legendary.core.internal.legendary.arena;
2+
3+
import java.util.Objects;
4+
import java.util.Set;
5+
import java.util.UUID;
6+
import java.util.concurrent.ConcurrentHashMap;
7+
8+
/**
9+
* Internal tracker of players whose participation has been revoked per encounter instance.
10+
*/
11+
public final class ArenaRevocationTracker {
12+
13+
private final ConcurrentHashMap<UUID, Set<UUID>> revokedByInstance = new ConcurrentHashMap<>();
14+
15+
public void revoke(UUID instanceId, UUID playerId) {
16+
Objects.requireNonNull(instanceId, "instanceId");
17+
Objects.requireNonNull(playerId, "playerId");
18+
revokedByInstance.computeIfAbsent(instanceId, ignored -> ConcurrentHashMap.newKeySet()).add(playerId);
19+
}
20+
21+
public boolean isRevoked(UUID instanceId, UUID playerId) {
22+
Objects.requireNonNull(instanceId, "instanceId");
23+
Objects.requireNonNull(playerId, "playerId");
24+
Set<UUID> set = revokedByInstance.get(instanceId);
25+
return set != null && set.contains(playerId);
26+
}
27+
28+
public void clearInstance(UUID instanceId) {
29+
Objects.requireNonNull(instanceId, "instanceId");
30+
revokedByInstance.remove(instanceId);
31+
}
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package io.github.legendaryforge.legendary.core.internal.legendary.arena;
2+
3+
import io.github.legendaryforge.legendary.core.api.encounter.EncounterInstance;
4+
import io.github.legendaryforge.legendary.core.api.encounter.EncounterKey;
5+
import io.github.legendaryforge.legendary.core.api.encounter.EncounterManager;
6+
import io.github.legendaryforge.legendary.core.api.encounter.EndReason;
7+
import io.github.legendaryforge.legendary.core.api.encounter.JoinResult;
8+
import io.github.legendaryforge.legendary.core.api.encounter.ParticipationRole;
9+
import java.util.Objects;
10+
import java.util.Optional;
11+
import java.util.UUID;
12+
import java.util.function.Predicate;
13+
14+
/**
15+
* Internal wrapper that prevents revoked players from rejoining as PARTICIPANT while an arena is ACTIVE.
16+
*
17+
* <p>Legendary-only and signal-driven. Platforms enforce behavior; Core gates participation state.
18+
*/
19+
public final class LegendaryRevokedRejoinEnforcingEncounterManager implements EncounterManager {
20+
21+
private final EncounterManager delegate;
22+
private final Predicate<UUID> isLegendaryInstance;
23+
private final PhaseGateInvariant phaseGate;
24+
private final ArenaRevocationTracker revocations;
25+
26+
public LegendaryRevokedRejoinEnforcingEncounterManager(
27+
EncounterManager delegate,
28+
Predicate<UUID> isLegendaryInstance,
29+
PhaseGateInvariant phaseGate,
30+
ArenaRevocationTracker revocations) {
31+
this.delegate = Objects.requireNonNull(delegate, "delegate");
32+
this.isLegendaryInstance = Objects.requireNonNull(isLegendaryInstance, "isLegendaryInstance");
33+
this.phaseGate = Objects.requireNonNull(phaseGate, "phaseGate");
34+
this.revocations = Objects.requireNonNull(revocations, "revocations");
35+
}
36+
37+
@Override
38+
public EncounterInstance create(
39+
io.github.legendaryforge.legendary.core.api.encounter.EncounterDefinition definition,
40+
io.github.legendaryforge.legendary.core.api.encounter.EncounterContext context) {
41+
return delegate.create(definition, context);
42+
}
43+
44+
@Override
45+
public JoinResult join(UUID playerId, EncounterInstance instance, ParticipationRole role) {
46+
Objects.requireNonNull(playerId, "playerId");
47+
Objects.requireNonNull(instance, "instance");
48+
Objects.requireNonNull(role, "role");
49+
50+
UUID instanceId = instance.instanceId();
51+
if (role == ParticipationRole.PARTICIPANT
52+
&& isLegendaryInstance.test(instanceId)
53+
&& phaseGate.phaseOf(instanceId).orElse(null) == ArenaPhase.ACTIVE
54+
&& revocations.isRevoked(instanceId, playerId)) {
55+
return JoinResult.DENIED_STATE;
56+
}
57+
58+
return delegate.join(playerId, instance, role);
59+
}
60+
61+
@Override
62+
public void leave(UUID playerId, EncounterInstance instance) {
63+
delegate.leave(playerId, instance);
64+
}
65+
66+
@Override
67+
public void end(EncounterInstance instance, EndReason reason) {
68+
delegate.end(instance, reason);
69+
}
70+
71+
@Override
72+
public Optional<EncounterInstance> byInstanceId(UUID instanceId) {
73+
return delegate.byInstanceId(instanceId);
74+
}
75+
76+
@Override
77+
public Optional<EncounterInstance> byKey(EncounterKey key) {
78+
return delegate.byKey(key);
79+
}
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package io.github.legendaryforge.legendary.core.internal.legendary.arena;
2+
3+
import io.github.legendaryforge.legendary.core.api.encounter.EncounterDefinition;
4+
import io.github.legendaryforge.legendary.core.api.encounter.EncounterInstance;
5+
import io.github.legendaryforge.legendary.core.api.encounter.EncounterKey;
6+
import io.github.legendaryforge.legendary.core.api.encounter.EncounterManager;
7+
import io.github.legendaryforge.legendary.core.api.encounter.EndReason;
8+
import io.github.legendaryforge.legendary.core.api.encounter.JoinResult;
9+
import io.github.legendaryforge.legendary.core.api.encounter.ParticipationRole;
10+
import io.github.legendaryforge.legendary.core.api.id.ResourceId;
11+
import java.lang.reflect.Proxy;
12+
import java.util.Optional;
13+
import java.util.Set;
14+
import java.util.UUID;
15+
import java.util.concurrent.ConcurrentHashMap;
16+
import java.util.concurrent.atomic.AtomicInteger;
17+
import org.junit.jupiter.api.Test;
18+
19+
import static org.junit.jupiter.api.Assertions.*;
20+
21+
final class LegendaryRevokedRejoinEnforcingEncounterManagerTest {
22+
23+
@Test
24+
void deniesParticipantRejoinWhenRevokedAndActive() {
25+
UUID instanceId = UUID.fromString("00000000-0000-0000-0000-000000000310");
26+
UUID playerId = UUID.fromString("00000000-0000-0000-0000-000000000311");
27+
28+
Set<UUID> legendaryInstances = ConcurrentHashMap.newKeySet();
29+
legendaryInstances.add(instanceId);
30+
31+
PhaseGateInvariant phaseGate = new PhaseGateInvariant();
32+
phaseGate.onStart(instanceId);
33+
34+
ArenaRevocationTracker revocations = new ArenaRevocationTracker();
35+
revocations.revoke(instanceId, playerId);
36+
37+
AtomicInteger delegateCalls = new AtomicInteger();
38+
EncounterManager delegate = new EncounterManager() {
39+
@Override public EncounterInstance create(EncounterDefinition definition, io.github.legendaryforge.legendary.core.api.encounter.EncounterContext context) { return null; }
40+
@Override public JoinResult join(UUID pid, EncounterInstance inst, ParticipationRole role) { delegateCalls.incrementAndGet(); return JoinResult.SUCCESS; }
41+
@Override public void leave(UUID pid, EncounterInstance inst) {}
42+
@Override public void end(EncounterInstance inst, EndReason reason) {}
43+
@Override public Optional<EncounterInstance> byInstanceId(UUID id) { return Optional.empty(); }
44+
@Override public Optional<EncounterInstance> byKey(EncounterKey key) { return Optional.empty(); }
45+
};
46+
47+
EncounterManager mgr = new LegendaryRevokedRejoinEnforcingEncounterManager(
48+
delegate, legendaryInstances::contains, phaseGate, revocations);
49+
50+
EncounterInstance instance = proxyEncounterInstance(instanceId);
51+
52+
assertEquals(JoinResult.DENIED_STATE, mgr.join(playerId, instance, ParticipationRole.PARTICIPANT));
53+
assertEquals(0, delegateCalls.get());
54+
55+
assertEquals(JoinResult.SUCCESS, mgr.join(playerId, instance, ParticipationRole.SPECTATOR));
56+
assertEquals(1, delegateCalls.get());
57+
}
58+
59+
private static EncounterInstance proxyEncounterInstance(UUID instanceId) {
60+
EncounterDefinition def = proxyEncounterDefinition();
61+
return (EncounterInstance) Proxy.newProxyInstance(
62+
EncounterInstance.class.getClassLoader(),
63+
new Class<?>[] { EncounterInstance.class },
64+
(proxy, method, args) -> {
65+
return switch (method.getName()) {
66+
case "instanceId" -> instanceId;
67+
case "definition" -> def;
68+
case "key" -> Optional.empty();
69+
default -> {
70+
Class<?> rt = method.getReturnType();
71+
if (rt.equals(Optional.class)) yield Optional.empty();
72+
if (rt.equals(int.class)) yield 0;
73+
if (rt.equals(boolean.class)) yield false;
74+
yield null;
75+
}
76+
};
77+
});
78+
}
79+
80+
private static EncounterDefinition proxyEncounterDefinition() {
81+
ResourceId id = ResourceId.parse("test:any");
82+
return (EncounterDefinition) Proxy.newProxyInstance(
83+
EncounterDefinition.class.getClassLoader(),
84+
new Class<?>[] { EncounterDefinition.class },
85+
(proxy, method, args) -> switch (method.getName()) {
86+
case "id" -> id;
87+
case "displayName" -> "test";
88+
case "accessPolicy" -> io.github.legendaryforge.legendary.core.api.encounter.EncounterAccessPolicy.PUBLIC;
89+
case "spectatorPolicy" -> io.github.legendaryforge.legendary.core.api.encounter.SpectatorPolicy.ALLOW_VIEW_ONLY;
90+
case "maxParticipants" -> 0;
91+
case "maxSpectators" -> 0;
92+
default -> null;
93+
});
94+
}
95+
}

0 commit comments

Comments
 (0)