Skip to content

Commit e947ee1

Browse files
committed
feat(core): implement delivery conditions on PluginMessageChannel senders
1 parent 7ea3747 commit e947ee1

2 files changed

Lines changed: 292 additions & 6 deletions

File tree

core/src/main/java/dev/objz/commandbridge/net/channel/PluginMessageChannel.java

Lines changed: 171 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.List;
1616
import java.util.Objects;
1717
import java.util.Set;
18+
import java.util.UUID;
1819
import java.util.concurrent.CompletableFuture;
1920
import java.util.concurrent.CompletionException;
2021

@@ -70,7 +71,7 @@ public Sender<P> to(Collection<Platform.ServerTarget> targets) {
7071

7172
@Override
7273
public Sender<P> toAll() {
73-
return new SingleTargetSender(Platform.BACKEND.target("*"));
74+
return new BroadcastSender();
7475
}
7576

7677
@Override
@@ -79,9 +80,27 @@ public Subscription listen(MessageListener<P> listener) {
7980
return listenerRegistrar.listen(listener);
8081
}
8182

82-
private PluginMessage toPluginMessage(P payload, boolean expectsResponse) {
83+
private PluginMessage toPluginMessage(P payload, boolean expectsResponse, UUID requirePlayer, UUID whenOnline) {
8384
return new PluginMessage(payloadType.getName(),
84-
Envelope.MAPPER.valueToTree(payload), expectsResponse);
85+
Envelope.MAPPER.valueToTree(payload), expectsResponse, requirePlayer, whenOnline, null);
86+
}
87+
88+
private PluginMessage toPluginMessage(P payload, boolean expectsResponse) {
89+
return toPluginMessage(payload, expectsResponse, null, null);
90+
}
91+
92+
private UUID resolveConditionUuid(P payload, UUID stored, boolean fromPayload) {
93+
if (!fromPayload) {
94+
return stored;
95+
}
96+
if (payload instanceof dev.objz.commandbridge.api.channel.command.CommandPayload cp) {
97+
UUID uuid = cp.player();
98+
if (uuid == null) {
99+
throw new IllegalStateException("Payload has no player UUID");
100+
}
101+
return uuid;
102+
}
103+
throw new IllegalStateException("No-arg condition requires CommandPayload with a player UUID");
85104
}
86105

87106
private P toPayload(PluginMessage response) {
@@ -95,15 +114,26 @@ private P toPayload(PluginMessage response) {
95114
private final class SingleTargetSender implements Sender<P> {
96115

97116
private final Platform.ServerTarget target;
117+
private UUID requirePlayerUuid;
118+
private boolean requirePlayerFromPayload;
119+
private UUID whenOnlineUuid;
120+
private boolean whenOnlineFromPayload;
98121

99122
SingleTargetSender(Platform.ServerTarget target) {
100123
this.target = target;
101124
}
102125

126+
private boolean hasCondition() {
127+
return requirePlayerUuid != null || requirePlayerFromPayload || whenOnlineUuid != null || whenOnlineFromPayload;
128+
}
129+
103130
@Override
104131
public CompletableFuture<Void> send(P payload) {
105132
Objects.requireNonNull(payload);
106-
return sendTransport.send(target, toPluginMessage(payload, false));
133+
UUID requirePlayer = PluginMessageChannel.this
134+
.resolveConditionUuid(payload, requirePlayerUuid, requirePlayerFromPayload);
135+
UUID whenOnline = PluginMessageChannel.this.resolveConditionUuid(payload, whenOnlineUuid, whenOnlineFromPayload);
136+
return sendTransport.send(target, toPluginMessage(payload, false, requirePlayer, whenOnline));
107137
}
108138

109139
@Override
@@ -115,14 +145,65 @@ public CompletableFuture<P> request(P payload) {
115145
public CompletableFuture<P> request(P payload, Duration timeout) {
116146
Objects.requireNonNull(payload);
117147
Objects.requireNonNull(timeout);
118-
return requestTransport.request(target, toPluginMessage(payload, true), timeout)
148+
if (hasCondition() && (whenOnlineUuid != null || whenOnlineFromPayload)) {
149+
throw new UnsupportedOperationException("whenOnline is not supported for request");
150+
}
151+
UUID requirePlayer = PluginMessageChannel.this
152+
.resolveConditionUuid(payload, requirePlayerUuid, requirePlayerFromPayload);
153+
return requestTransport.request(target, toPluginMessage(payload, true, requirePlayer, null), timeout)
119154
.thenApply(PluginMessageChannel.this::toPayload);
120155
}
156+
157+
@Override
158+
public Sender<P> requirePlayer(UUID player) {
159+
Objects.requireNonNull(player);
160+
if (whenOnlineUuid != null || whenOnlineFromPayload) {
161+
throw new IllegalStateException("Cannot combine requirePlayer and whenOnline");
162+
}
163+
requirePlayerUuid = player;
164+
requirePlayerFromPayload = false;
165+
return this;
166+
}
167+
168+
@Override
169+
public Sender<P> requirePlayer() {
170+
if (whenOnlineUuid != null || whenOnlineFromPayload) {
171+
throw new IllegalStateException("Cannot combine requirePlayer and whenOnline");
172+
}
173+
requirePlayerFromPayload = true;
174+
requirePlayerUuid = null;
175+
return this;
176+
}
177+
178+
@Override
179+
public Sender<P> whenOnline(UUID player) {
180+
Objects.requireNonNull(player);
181+
if (requirePlayerUuid != null || requirePlayerFromPayload) {
182+
throw new IllegalStateException("Cannot combine requirePlayer and whenOnline");
183+
}
184+
whenOnlineUuid = player;
185+
whenOnlineFromPayload = false;
186+
return this;
187+
}
188+
189+
@Override
190+
public Sender<P> whenOnline() {
191+
if (requirePlayerUuid != null || requirePlayerFromPayload) {
192+
throw new IllegalStateException("Cannot combine requirePlayer and whenOnline");
193+
}
194+
whenOnlineFromPayload = true;
195+
whenOnlineUuid = null;
196+
return this;
197+
}
121198
}
122199

123200
private final class MultiTargetSender implements Sender<P> {
124201

125202
private final Set<Platform.ServerTarget> targets;
203+
private UUID requirePlayerUuid;
204+
private boolean requirePlayerFromPayload;
205+
private UUID whenOnlineUuid;
206+
private boolean whenOnlineFromPayload;
126207

127208
MultiTargetSender(Set<Platform.ServerTarget> targets) {
128209
this.targets = targets;
@@ -131,7 +212,10 @@ private final class MultiTargetSender implements Sender<P> {
131212
@Override
132213
public CompletableFuture<Void> send(P payload) {
133214
Objects.requireNonNull(payload);
134-
PluginMessage message = toPluginMessage(payload, false);
215+
UUID requirePlayer = PluginMessageChannel.this
216+
.resolveConditionUuid(payload, requirePlayerUuid, requirePlayerFromPayload);
217+
UUID whenOnline = PluginMessageChannel.this.resolveConditionUuid(payload, whenOnlineUuid, whenOnlineFromPayload);
218+
PluginMessage message = toPluginMessage(payload, false, requirePlayer, whenOnline);
135219
List<CompletableFuture<Void>> futures = new ArrayList<>();
136220
for (Platform.ServerTarget target : targets) {
137221
futures.add(sendTransport.send(target, message));
@@ -148,5 +232,86 @@ public CompletableFuture<P> request(P payload) {
148232
public CompletableFuture<P> request(P payload, Duration timeout) {
149233
throw new UnsupportedOperationException("Request is only supported for single-target senders");
150234
}
235+
236+
@Override
237+
public Sender<P> requirePlayer(UUID player) {
238+
Objects.requireNonNull(player);
239+
if (whenOnlineUuid != null || whenOnlineFromPayload) {
240+
throw new IllegalStateException("Cannot combine requirePlayer and whenOnline");
241+
}
242+
requirePlayerUuid = player;
243+
requirePlayerFromPayload = false;
244+
return this;
245+
}
246+
247+
@Override
248+
public Sender<P> requirePlayer() {
249+
if (whenOnlineUuid != null || whenOnlineFromPayload) {
250+
throw new IllegalStateException("Cannot combine requirePlayer and whenOnline");
251+
}
252+
requirePlayerFromPayload = true;
253+
requirePlayerUuid = null;
254+
return this;
255+
}
256+
257+
@Override
258+
public Sender<P> whenOnline(UUID player) {
259+
Objects.requireNonNull(player);
260+
if (requirePlayerUuid != null || requirePlayerFromPayload) {
261+
throw new IllegalStateException("Cannot combine requirePlayer and whenOnline");
262+
}
263+
whenOnlineUuid = player;
264+
whenOnlineFromPayload = false;
265+
return this;
266+
}
267+
268+
@Override
269+
public Sender<P> whenOnline() {
270+
if (requirePlayerUuid != null || requirePlayerFromPayload) {
271+
throw new IllegalStateException("Cannot combine requirePlayer and whenOnline");
272+
}
273+
whenOnlineFromPayload = true;
274+
whenOnlineUuid = null;
275+
return this;
276+
}
277+
}
278+
279+
private final class BroadcastSender implements Sender<P> {
280+
281+
@Override
282+
public CompletableFuture<Void> send(P payload) {
283+
Objects.requireNonNull(payload);
284+
return sendTransport.send(Platform.BACKEND.target("*"), toPluginMessage(payload, false));
285+
}
286+
287+
@Override
288+
public CompletableFuture<P> request(P payload) {
289+
throw new UnsupportedOperationException("Request is not supported for broadcast senders");
290+
}
291+
292+
@Override
293+
public CompletableFuture<P> request(P payload, Duration timeout) {
294+
throw new UnsupportedOperationException("Request is not supported for broadcast senders");
295+
}
296+
297+
@Override
298+
public Sender<P> requirePlayer(UUID player) {
299+
throw new UnsupportedOperationException("Delivery conditions are not supported for broadcast senders");
300+
}
301+
302+
@Override
303+
public Sender<P> requirePlayer() {
304+
throw new UnsupportedOperationException("Delivery conditions are not supported for broadcast senders");
305+
}
306+
307+
@Override
308+
public Sender<P> whenOnline(UUID player) {
309+
throw new UnsupportedOperationException("Delivery conditions are not supported for broadcast senders");
310+
}
311+
312+
@Override
313+
public Sender<P> whenOnline() {
314+
throw new UnsupportedOperationException("Delivery conditions are not supported for broadcast senders");
315+
}
151316
}
152317
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package dev.objz.commandbridge.net.channel;
2+
3+
import static dev.objz.commandbridge.api.platform.Platform.backend;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.assertSame;
6+
import static org.junit.jupiter.api.Assertions.assertThrows;
7+
8+
import dev.objz.commandbridge.api.channel.MessageChannel.Sender;
9+
import dev.objz.commandbridge.api.channel.command.CommandPayload;
10+
import dev.objz.commandbridge.api.channel.command.RunAs;
11+
import dev.objz.commandbridge.net.payloads.PluginMessage;
12+
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
import java.util.UUID;
16+
import java.util.concurrent.CompletableFuture;
17+
18+
import org.junit.jupiter.api.Test;
19+
20+
class PluginMessageChannelTest {
21+
22+
@Test
23+
void requirePlayerChaining() {
24+
List<PluginMessage> sent = new ArrayList<>();
25+
PluginMessageChannel<CommandPayload> channel = createChannel(sent);
26+
Sender<CommandPayload> sender = channel.to(List.of(backend("s1")));
27+
28+
Sender<CommandPayload> chained = sender.requirePlayer(UUID.randomUUID());
29+
30+
assertSame(sender, chained);
31+
}
32+
33+
@Test
34+
void mutualExclusivity() {
35+
List<PluginMessage> sent = new ArrayList<>();
36+
PluginMessageChannel<CommandPayload> channel = createChannel(sent);
37+
Sender<CommandPayload> sender = channel.to(List.of(backend("s1")));
38+
39+
sender.requirePlayer(UUID.randomUUID());
40+
41+
assertThrows(IllegalStateException.class, () -> sender.whenOnline(UUID.randomUUID()));
42+
}
43+
44+
@Test
45+
void broadcastSenderRejectsConditions() {
46+
List<PluginMessage> sent = new ArrayList<>();
47+
PluginMessageChannel<CommandPayload> channel = createChannel(sent);
48+
49+
assertThrows(UnsupportedOperationException.class, () -> channel.toAll().requirePlayer(UUID.randomUUID()));
50+
}
51+
52+
@Test
53+
void singleTargetRequirePlayerPropagated() {
54+
List<PluginMessage> sent = new ArrayList<>();
55+
PluginMessageChannel<CommandPayload> channel = createChannel(sent);
56+
UUID playerUuid = UUID.randomUUID();
57+
58+
channel.to(List.of(backend("s1"))).requirePlayer(playerUuid).send(new CommandPayload("cmd", RunAs.CONSOLE)).join();
59+
60+
assertEquals(1, sent.size());
61+
assertEquals(playerUuid, sent.get(0).requirePlayer());
62+
}
63+
64+
@Test
65+
void noArgRequirePlayerFromPayload() {
66+
List<PluginMessage> sent = new ArrayList<>();
67+
PluginMessageChannel<CommandPayload> channel = createChannel(sent);
68+
UUID playerUuid = UUID.randomUUID();
69+
70+
channel.to(List.of(backend("s1"))).requirePlayer().send(new CommandPayload("cmd", RunAs.PLAYER, playerUuid)).join();
71+
72+
assertEquals(1, sent.size());
73+
assertEquals(playerUuid, sent.get(0).requirePlayer());
74+
}
75+
76+
@Test
77+
void noArgWithNullPlayerThrows() {
78+
List<PluginMessage> sent = new ArrayList<>();
79+
PluginMessageChannel<CommandPayload> channel = createChannel(sent);
80+
Sender<CommandPayload> sender = channel.to(List.of(backend("s1"))).requirePlayer();
81+
82+
assertThrows(IllegalStateException.class, () -> sender.send(new CommandPayload("cmd", RunAs.CONSOLE)));
83+
}
84+
85+
@Test
86+
void whenOnlineRequestThrows() {
87+
List<PluginMessage> sent = new ArrayList<>();
88+
PluginMessageChannel<CommandPayload> channel = createChannel(sent);
89+
Sender<CommandPayload> sender = channel.to(List.of(backend("s1"))).whenOnline(UUID.randomUUID());
90+
91+
assertThrows(UnsupportedOperationException.class, () -> sender.request(new CommandPayload("cmd", RunAs.CONSOLE)));
92+
}
93+
94+
@Test
95+
void multiTargetAcceptsConditions() {
96+
List<PluginMessage> sent = new ArrayList<>();
97+
PluginMessageChannel<CommandPayload> channel = createChannel(sent);
98+
UUID playerUuid = UUID.randomUUID();
99+
100+
channel.to(List.of(backend("s1"), backend("s2")))
101+
.requirePlayer(playerUuid)
102+
.send(new CommandPayload("cmd", RunAs.CONSOLE))
103+
.join();
104+
105+
assertEquals(2, sent.size());
106+
assertEquals(playerUuid, sent.get(0).requirePlayer());
107+
assertEquals(playerUuid, sent.get(1).requirePlayer());
108+
}
109+
110+
private PluginMessageChannel<CommandPayload> createChannel(List<PluginMessage> sent) {
111+
return new PluginMessageChannel<>(
112+
CommandPayload.class,
113+
(target, msg) -> {
114+
sent.add(msg);
115+
return CompletableFuture.completedFuture(null);
116+
},
117+
(target, msg, timeout) -> CompletableFuture.completedFuture(msg),
118+
listener -> () -> {
119+
});
120+
}
121+
}

0 commit comments

Comments
 (0)