Skip to content

Commit ce4f3d4

Browse files
Internal: gate arena invariants by legendary instance ids
1 parent dfcd90d commit ce4f3d4

File tree

6 files changed

+88
-53
lines changed

6 files changed

+88
-53
lines changed

docs/internal/arena-invariants.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ They must not introduce new public Core APIs and must not leak behavior into non
3030

3131
## Legendary-only gating (LOCKED)
3232
- Arena invariants apply only to Legendary encounters.
33-
- Legendary detection is captured at encounter creation time by tracking definition IDs when:
33+
- Legendary detection is captured at encounter creation time by tracking instance IDs when:
3434
- definition instanceof LegendaryEncounterDefinition
3535
- Gating is enforced internally in DefaultCoreRuntime wiring.
3636

src/main/java/io/github/legendaryforge/legendary/core/internal/legendary/arena/ArenaInvariantBridge.java

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import java.util.Objects;
1212
import java.util.UUID;
1313
import java.util.concurrent.ConcurrentHashMap;
14+
import java.util.function.Consumer;
15+
import java.util.function.Predicate;
1416

1517
/**
1618
* Internal bridge that forwards encounter lifecycle signals to arena invariants.
@@ -20,17 +22,32 @@
2022
*/
2123
public final class ArenaInvariantBridge {
2224

25+
private static boolean always(UUID id) { return true; }
26+
27+
private static void noop(UUID id) {}
28+
2329
private final ArenaInvariantRegistry registry;
30+
private final Predicate<UUID> applyFilter;
31+
private final Consumer<UUID> onCleanupPost;
2432

2533
// Cleanup event only carries instanceId; this mapping routes cleanup to invariants.
2634
private final ConcurrentHashMap<UUID, ResourceId> instanceToDefinition = new ConcurrentHashMap<>();
2735

2836
public ArenaInvariantBridge(ArenaInvariantRegistry registry) {
37+
this(registry, ArenaInvariantBridge::always, ArenaInvariantBridge::noop);
38+
}
39+
40+
public ArenaInvariantBridge(ArenaInvariantRegistry registry, Predicate<UUID> applyFilter, Consumer<UUID> onCleanupPost) {
2941
this.registry = Objects.requireNonNull(registry, "registry");
42+
this.applyFilter = Objects.requireNonNull(applyFilter, "applyFilter");
43+
this.onCleanupPost = Objects.requireNonNull(onCleanupPost, "onCleanupPost");
3044
}
3145

3246
public void onStarted(EncounterStartedEvent event) {
3347
Objects.requireNonNull(event, "event");
48+
if (!applyFilter.test(event.instanceId())) {
49+
return;
50+
}
3451
instanceToDefinition.put(event.instanceId(), event.definitionId());
3552
for (ArenaInvariant inv : registry.invariantsFor(event.definitionId())) {
3653
inv.onStart(event.instanceId());
@@ -39,27 +56,39 @@ public void onStarted(EncounterStartedEvent event) {
3956

4057
public void onEnded(EncounterEndedEvent event) {
4158
Objects.requireNonNull(event, "event");
59+
if (!applyFilter.test(event.instanceId())) {
60+
return;
61+
}
4262
for (ArenaInvariant inv : registry.invariantsFor(event.definitionId())) {
4363
inv.onEnd(event.instanceId());
4464
}
4565
}
4666

4767
public void onCleanup(EncounterCleanupEvent event) {
4868
Objects.requireNonNull(event, "event");
49-
ResourceId defId = instanceToDefinition.remove(event.instanceId());
69+
UUID instanceId = event.instanceId();
70+
ResourceId defId = instanceToDefinition.remove(instanceId);
5071
if (defId == null) {
72+
onCleanupPost.accept(instanceId);
5173
return;
5274
}
53-
for (ArenaInvariant inv : registry.invariantsFor(defId)) {
54-
inv.onCleanup(event.instanceId());
75+
if (applyFilter.test(instanceId)) {
76+
for (ArenaInvariant inv : registry.invariantsFor(defId)) {
77+
inv.onCleanup(instanceId);
78+
}
5579
}
80+
onCleanupPost.accept(instanceId);
5681
}
5782

5883
public static List<Subscription> bind(EventBus bus, ArenaInvariantRegistry registry) {
84+
return bind(bus, registry, ArenaInvariantBridge::always, ArenaInvariantBridge::noop);
85+
}
86+
87+
public static List<Subscription> bind(EventBus bus, ArenaInvariantRegistry registry, Predicate<UUID> applyFilter, Consumer<UUID> onCleanupPost) {
5988
Objects.requireNonNull(bus, "bus");
6089
Objects.requireNonNull(registry, "registry");
6190

62-
ArenaInvariantBridge bridge = new ArenaInvariantBridge(registry);
91+
ArenaInvariantBridge bridge = new ArenaInvariantBridge(registry, applyFilter, onCleanupPost);
6392
List<Subscription> subs = new ArrayList<>(3);
6493

6594
subs.add(bus.subscribe(EncounterStartedEvent.class, bridge::onStarted));

src/main/java/io/github/legendaryforge/legendary/core/internal/legendary/arena/LegendaryDefinitionTrackingEncounterManager.java renamed to src/main/java/io/github/legendaryforge/legendary/core/internal/legendary/arena/LegendaryInstanceTrackingEncounterManager.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,41 @@
88
import io.github.legendaryforge.legendary.core.api.encounter.EndReason;
99
import io.github.legendaryforge.legendary.core.api.encounter.JoinResult;
1010
import io.github.legendaryforge.legendary.core.api.encounter.ParticipationRole;
11-
import io.github.legendaryforge.legendary.core.api.id.ResourceId;
1211
import io.github.legendaryforge.legendary.core.api.legendary.definition.LegendaryEncounterDefinition;
1312
import java.util.Objects;
1413
import java.util.Optional;
1514
import java.util.Set;
1615
import java.util.UUID;
1716

1817
/**
19-
* Internal decorator that tracks which encounter definition ids are legendary based on runtime create() calls.
18+
* Internal decorator that tracks which encounter instance ids are legendary based on runtime create() calls.
2019
*
21-
* <p>This provides a deterministic, low-overhead gating mechanism for internal arena invariants without introducing
20+
* <p>This provides a deterministic, bounded gating mechanism for internal arena invariants without introducing
2221
* new public Core APIs or registries.
2322
*/
24-
public final class LegendaryDefinitionTrackingEncounterManager implements EncounterManager {
23+
public final class LegendaryInstanceTrackingEncounterManager implements EncounterManager {
2524

2625
private final EncounterManager delegate;
27-
private final Set<ResourceId> legendaryDefinitionIds;
26+
private final Set<UUID> legendaryInstanceIds;
2827

29-
public LegendaryDefinitionTrackingEncounterManager(EncounterManager delegate, Set<ResourceId> legendaryDefinitionIds) {
28+
public LegendaryInstanceTrackingEncounterManager(EncounterManager delegate, Set<UUID> legendaryInstanceIds) {
3029
this.delegate = Objects.requireNonNull(delegate, "delegate");
31-
this.legendaryDefinitionIds = Objects.requireNonNull(legendaryDefinitionIds, "legendaryDefinitionIds");
30+
this.legendaryInstanceIds = Objects.requireNonNull(legendaryInstanceIds, "legendaryInstanceIds");
3231
}
3332

34-
public boolean isLegendary(ResourceId definitionId) {
35-
Objects.requireNonNull(definitionId, "definitionId");
36-
return legendaryDefinitionIds.contains(definitionId);
33+
public boolean isLegendary(UUID instanceId) {
34+
Objects.requireNonNull(instanceId, "instanceId");
35+
return legendaryInstanceIds.contains(instanceId);
3736
}
3837

3938
@Override
4039
public EncounterInstance create(EncounterDefinition definition, EncounterContext context) {
4140
Objects.requireNonNull(definition, "definition");
41+
EncounterInstance instance = delegate.create(definition, context);
4242
if (definition instanceof LegendaryEncounterDefinition) {
43-
legendaryDefinitionIds.add(definition.id());
43+
legendaryInstanceIds.add(instance.instanceId());
4444
}
45-
return delegate.create(definition, context);
45+
return instance;
4646
}
4747

4848
@Override

src/main/java/io/github/legendaryforge/legendary/core/internal/runtime/DefaultCoreRuntime.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import java.time.Clock;
2626
import java.util.concurrent.ConcurrentHashMap;
2727
import java.util.Set;
28-
import io.github.legendaryforge.legendary.core.internal.legendary.arena.LegendaryDefinitionTrackingEncounterManager;
28+
import io.github.legendaryforge.legendary.core.internal.legendary.arena.LegendaryInstanceTrackingEncounterManager;
2929
import io.github.legendaryforge.legendary.core.api.id.ResourceId;
3030
import java.util.Objects;
3131
import java.util.Optional;
@@ -85,21 +85,21 @@ public DefaultCoreRuntime(Optional<PlayerDirectory> players, Optional<PartyDirec
8585
io.github.legendaryforge.legendary.core.api.encounter.event.EncounterEndedEvent.class,
8686
durationTelemetry::onEnded);
8787

88-
Set<ResourceId> legendaryDefinitionIds = ConcurrentHashMap.newKeySet();
88+
Set<java.util.UUID> legendaryInstanceIds = ConcurrentHashMap.newKeySet();
8989
PhaseGateInvariant phaseGate = new PhaseGateInvariant();
9090

9191
ArenaInvariantRegistry arenaRegistry = definitionId -> {
9292
Objects.requireNonNull(definitionId, "definitionId");
93-
return legendaryDefinitionIds.contains(definitionId) ? java.util.List.of(phaseGate) : java.util.List.of();
93+
return java.util.List.of(phaseGate);
9494
};
9595

96-
ArenaInvariantBridge.bind(bus, arenaRegistry);
96+
ArenaInvariantBridge.bind(bus, arenaRegistry, legendaryInstanceIds::contains, legendaryInstanceIds::remove);
9797

9898
EncounterManager base = new DefaultEncounterManager(players, parties, Optional.of(bus));
9999
EncounterManager startGated = new LegendaryStartGatingEncounterManager(
100100
base, new DefaultLegendaryStartPolicy(), new NoopLegendaryPenaltyStatus());
101101
EncounterManager enforced = new LegendaryAccessEnforcingEncounterManager(startGated, new DefaultLegendaryAccessPolicy());
102-
this.encounters = new LegendaryDefinitionTrackingEncounterManager(enforced, legendaryDefinitionIds);
102+
this.encounters = new LegendaryInstanceTrackingEncounterManager(enforced, legendaryInstanceIds);
103103

104104
this.players = Objects.requireNonNull(players, "players");
105105
this.parties = Objects.requireNonNull(parties, "parties");

src/test/java/io/github/legendaryforge/legendary/core/internal/legendary/arena/LegendaryDefinitionTrackingEncounterManagerTest.java renamed to src/test/java/io/github/legendaryforge/legendary/core/internal/legendary/arena/LegendaryInstanceTrackingEncounterManagerTest.java

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.legendaryforge.legendary.core.internal.legendary.arena;
22

3+
import io.github.legendaryforge.legendary.core.api.encounter.EncounterAccessPolicy;
34
import io.github.legendaryforge.legendary.core.api.encounter.EncounterContext;
45
import io.github.legendaryforge.legendary.core.api.encounter.EncounterDefinition;
56
import io.github.legendaryforge.legendary.core.api.encounter.EncounterInstance;
@@ -9,7 +10,6 @@
910
import io.github.legendaryforge.legendary.core.api.encounter.JoinResult;
1011
import io.github.legendaryforge.legendary.core.api.encounter.ParticipationRole;
1112
import io.github.legendaryforge.legendary.core.api.encounter.SpectatorPolicy;
12-
import io.github.legendaryforge.legendary.core.api.encounter.EncounterAccessPolicy;
1313
import io.github.legendaryforge.legendary.core.api.id.ResourceId;
1414
import io.github.legendaryforge.legendary.core.api.legendary.definition.LegendaryEncounterDefinition;
1515
import java.lang.reflect.Proxy;
@@ -21,16 +21,28 @@
2121

2222
import static org.junit.jupiter.api.Assertions.*;
2323

24-
final class LegendaryDefinitionTrackingEncounterManagerTest {
24+
final class LegendaryInstanceTrackingEncounterManagerTest {
2525

2626
@Test
27-
void tracksLegendaryDefinitionIdsOnCreate() {
28-
Set<ResourceId> ids = ConcurrentHashMap.newKeySet();
27+
void tracksLegendaryInstanceIdsOnCreate() {
28+
Set<UUID> ids = ConcurrentHashMap.newKeySet();
29+
30+
UUID normalInstanceId = UUID.fromString("00000000-0000-0000-0000-000000000001");
31+
UUID legendaryInstanceId = UUID.fromString("00000000-0000-0000-0000-000000000002");
32+
33+
ResourceId normalId = ResourceId.parse("test:normal_encounter");
34+
ResourceId legendaryId = ResourceId.parse("test:legendary_encounter");
35+
36+
EncounterDefinition normalDef = proxyEncounterDefinition(EncounterDefinition.class, normalId);
37+
LegendaryEncounterDefinition legendaryDef = proxyEncounterDefinition(LegendaryEncounterDefinition.class, legendaryId);
2938

3039
EncounterManager delegate = new EncounterManager() {
3140
@Override
3241
public EncounterInstance create(EncounterDefinition definition, EncounterContext context) {
33-
return proxyEncounterInstance();
42+
if (definition instanceof LegendaryEncounterDefinition) {
43+
return proxyEncounterInstance(legendaryInstanceId);
44+
}
45+
return proxyEncounterInstance(normalInstanceId);
3446
}
3547

3648
@Override
@@ -55,29 +67,25 @@ public Optional<EncounterInstance> byKey(EncounterKey key) {
5567
}
5668
};
5769

58-
LegendaryDefinitionTrackingEncounterManager mgr =
59-
new LegendaryDefinitionTrackingEncounterManager(delegate, ids);
60-
61-
ResourceId normalId = ResourceId.parse("test:normal_encounter");
62-
ResourceId legendaryId = ResourceId.parse("test:legendary_encounter");
63-
64-
EncounterDefinition normalDef = proxyEncounterDefinition(EncounterDefinition.class, normalId);
65-
LegendaryEncounterDefinition legendaryDef = proxyEncounterDefinition(LegendaryEncounterDefinition.class, legendaryId);
70+
LegendaryInstanceTrackingEncounterManager mgr =
71+
new LegendaryInstanceTrackingEncounterManager(delegate, ids);
6672

67-
mgr.create(normalDef, null);
68-
assertFalse(mgr.isLegendary(normalId));
73+
EncounterInstance normalInstance = mgr.create(normalDef, null);
74+
assertFalse(mgr.isLegendary(normalInstance.instanceId()));
6975

70-
mgr.create(legendaryDef, null);
71-
assertTrue(mgr.isLegendary(legendaryId));
76+
EncounterInstance legendaryInstance = mgr.create(legendaryDef, null);
77+
assertTrue(mgr.isLegendary(legendaryInstance.instanceId()));
7278
}
7379

74-
private static EncounterInstance proxyEncounterInstance() {
80+
private static EncounterInstance proxyEncounterInstance(UUID instanceId) {
7581
return (EncounterInstance) Proxy.newProxyInstance(
7682
EncounterInstance.class.getClassLoader(),
7783
new Class<?>[] { EncounterInstance.class },
7884
(proxy, method, args) -> {
85+
if ("instanceId".equals(method.getName()) && method.getReturnType().equals(UUID.class)) {
86+
return instanceId;
87+
}
7988
Class<?> rt = method.getReturnType();
80-
if (rt.equals(UUID.class)) return UUID.randomUUID();
8189
if (rt.equals(Optional.class)) return Optional.empty();
8290
if (rt.equals(int.class)) return 0;
8391
if (rt.equals(boolean.class)) return false;
@@ -89,16 +97,14 @@ private static <T> T proxyEncounterDefinition(Class<T> type, ResourceId id) {
8997
return type.cast(Proxy.newProxyInstance(
9098
type.getClassLoader(),
9199
new Class<?>[] { type },
92-
(proxy, method, args) -> {
93-
return switch (method.getName()) {
94-
case "id" -> id;
95-
case "displayName" -> "test";
96-
case "accessPolicy" -> EncounterAccessPolicy.PUBLIC;
97-
case "spectatorPolicy" -> SpectatorPolicy.ALLOW_VIEW_ONLY;
98-
case "maxParticipants" -> 0;
99-
case "maxSpectators" -> 0;
100-
default -> null;
101-
};
100+
(proxy, method, args) -> switch (method.getName()) {
101+
case "id" -> id;
102+
case "displayName" -> "test";
103+
case "accessPolicy" -> EncounterAccessPolicy.PUBLIC;
104+
case "spectatorPolicy" -> SpectatorPolicy.ALLOW_VIEW_ONLY;
105+
case "maxParticipants" -> 0;
106+
case "maxSpectators" -> 0;
107+
default -> null;
102108
}));
103109
}
104110
}

src/test/java/io/github/legendaryforge/legendary/core/internal/runtime/DefaultCoreRuntimeWiringTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import io.github.legendaryforge.legendary.core.api.encounter.EncounterManager;
77
import io.github.legendaryforge.legendary.core.api.platform.CoreRuntime;
88
import io.github.legendaryforge.legendary.core.internal.encounter.DefaultEncounterManager;
9-
import io.github.legendaryforge.legendary.core.internal.legendary.arena.LegendaryDefinitionTrackingEncounterManager;
9+
import io.github.legendaryforge.legendary.core.internal.legendary.arena.LegendaryInstanceTrackingEncounterManager;
1010
import io.github.legendaryforge.legendary.core.internal.legendary.manager.LegendaryAccessEnforcingEncounterManager;
1111
import io.github.legendaryforge.legendary.core.internal.legendary.start.LegendaryStartGatingEncounterManager;
1212
import java.lang.reflect.Field;
@@ -30,7 +30,7 @@ void defaultConstructorWiresLegendaryEncounterManagersInCorrectOrder() {
3030
CoreRuntime runtime = new DefaultCoreRuntime();
3131

3232
EncounterManager encounters = runtime.encounters();
33-
assertTrue(encounters instanceof LegendaryDefinitionTrackingEncounterManager, "top-level should track legendary definitions");
33+
assertTrue(encounters instanceof LegendaryInstanceTrackingEncounterManager, "top-level should track legendary instances");
3434

3535
EncounterManager enforcing = readDelegate(encounters);
3636
assertTrue(enforcing instanceof LegendaryAccessEnforcingEncounterManager, "second-level should enforce access");

0 commit comments

Comments
 (0)