Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
10 changes: 9 additions & 1 deletion src/main/java/com/faforever/client/preferences/VetoKey.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
package com.faforever.client.preferences;

import com.faforever.client.domain.api.MapPoolAssignment;

public record VetoKey(
int matchmakerQueueMapPoolId,
int mapPoolMapVersionId
) {}
) {

public static VetoKey of(MapPoolAssignment assignment) {
return new VetoKey(assignment.mapPool().mapPool().id(), assignment.id());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
import com.faforever.client.domain.api.MapPoolAssignment;
import com.faforever.client.domain.api.MapVersion;
import com.faforever.client.fx.ImageViewHelper;
import com.faforever.client.fx.NodeController;
import com.faforever.client.i18n.I18n;
import com.faforever.client.map.MapService;
import com.faforever.client.fx.NodeController;
import com.faforever.client.map.MapService.PreviewSize;
import com.faforever.client.map.generator.MapGeneratorService;
import com.faforever.client.preferences.MatchmakerPrefs;
Expand All @@ -25,13 +25,14 @@
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import java.util.function.Consumer;

/**
Expand Down Expand Up @@ -101,9 +102,10 @@ void setVetoModeEnabled(BooleanProperty vetoModeEnabled) {
public void bindVetoModeDependentProperties() {
vetoesBox.mouseTransparentProperty().bind(vetoModeEnabled.not());
vetoesBox.visibleProperty().bind(vetoModeEnabled.or(tokenCount.greaterThan(0)));
root.cursorProperty().bind(Bindings.when(
Bindings.createBooleanBinding(() -> isGeneratedMap.getValue() || vetoModeEnabled.get(), isGeneratedMap, vetoModeEnabled)
).then(Cursor.DEFAULT).otherwise(Cursor.HAND));
root.cursorProperty()
.bind(Bindings.when(
Bindings.createBooleanBinding(() -> isGeneratedMap.getValue() || vetoModeEnabled.get(), isGeneratedMap,
vetoModeEnabled)).then(Cursor.DEFAULT).otherwise(Cursor.HAND));
}

@Override
Expand All @@ -112,9 +114,9 @@ protected void onInitialize() {
isGeneratedMap = mapObservable.map(map -> mapGeneratorService.isGeneratedMap(map.displayName())).orElse(false);

thumbnailImageView.imageProperty()
.bind(assignment
.map(assignmentBean -> mapService.loadPreview(assignmentBean.mapVersion(), PreviewSize.SMALL))
.flatMap(imageViewHelper::createPlaceholderImageOnErrorObservable));
.bind(assignment.map(
assignmentBean -> mapService.loadPreview(assignmentBean.mapVersion(), PreviewSize.SMALL))
.flatMap(imageViewHelper::createPlaceholderImageOnErrorObservable));

nameLabel.textProperty().bind(mapObservable.map(map -> {
if (isGeneratedMap.getValue()) {
Expand All @@ -123,9 +125,7 @@ protected void onInitialize() {
return map.displayName();
}));

authorBox.visibleProperty()
.bind(mapObservable.map(
map -> (map.author() != null) || isGeneratedMap.getValue()));
authorBox.visibleProperty().bind(mapObservable.map(map -> (map.author() != null) || isGeneratedMap.getValue()));
authorBox.managedProperty().bind(authorBox.visibleProperty());

authorLabel.textProperty().bind(mapObservable.map(map -> {
Expand Down Expand Up @@ -165,27 +165,26 @@ protected void onInitialize() {
});

vetoButton.setOnAction(event -> {
if (assignment.getValue() == null) {
MapPoolAssignment value = assignment.getValue();
if (value == null) {
return;
}
int currentTokenCount = tokenCount.get();
int currentMaxPerMap = maxPerMap.get();
if ((isMaxPerMapDynamic.get() || currentTokenCount < currentMaxPerMap) && currentTokenCount < vetoTokensMax.get() && vetoTokensLeft.getValue() > 0) {
teamMatchmakingService.setTokensForMap(
new VetoKey(assignment.getValue().mapPool().mapPool().id(), assignment.getValue().id()),
currentTokenCount + 1);
teamMatchmakingService.setTokensForMap(VetoKey.of(value), currentTokenCount + 1);
}
event.consume();
});

minusButton.setOnAction(event -> {
if (assignment.getValue() == null) {
MapPoolAssignment value = assignment.getValue();
if (value == null) {
return;
}
int current = tokenCount.get();
if (current > 0) {
teamMatchmakingService.setTokensForMap(
new VetoKey(assignment.getValue().mapPool().mapPool().id(), assignment.getValue().id()), current - 1);
teamMatchmakingService.setTokensForMap(VetoKey.of(value), current - 1);
}
event.consume();
});
Expand Down Expand Up @@ -213,11 +212,12 @@ private void updateBannedState() {
}

private void updateVetoes() {
if (assignment.getValue() == null) {
MapPoolAssignment value = assignment.getValue();
if (value == null) {
tokenCount.set(0);
return;
}
VetoKey key = new VetoKey(assignment.getValue().mapPool().mapPool().id(), assignment.getValue().id());
VetoKey key = VetoKey.of(value);
int usedTokens = matchmakerPrefs.getAppliedVetoes().getOrDefault(key, 0);
tokenCount.set(usedTokens);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
Expand Down Expand Up @@ -245,12 +246,6 @@ public void afterPropertiesSet() throws Exception {
}
});

matchmakerPrefs.getAppliedVetoes().subscribe(()->{
if (fafServerAccessor.getConnectionState() == ConnectionState.CONNECTED) {
sendVetoes();
}
});

party.ownerProperty().subscribe((oldValue, newValue) -> {
if (oldValue != null) {
chatService.leaveChannel("#" + oldValue.getUsername() + PARTY_CHANNEL_SUFFIX);
Expand All @@ -274,27 +269,37 @@ private void sendVetoes() {
fafServerAccessor.setPlayerVetoes(getVetoesAsList());
}

public void setTokensForMap(VetoKey vetoKey, Integer vetoTokensApplied) {
matchmakerPrefs.getAppliedVetoes().put(vetoKey, vetoTokensApplied);
public void setTokensForMap(VetoKey key, int vetoTokensApplied) {
Integer previousValue;
if (vetoTokensApplied == 0) {
previousValue = matchmakerPrefs.getAppliedVetoes().remove(key);
} else {
previousValue = matchmakerPrefs.getAppliedVetoes().put(key, vetoTokensApplied);
}

if ((previousValue == null && vetoTokensApplied > 0) || (previousValue != null && previousValue != vetoTokensApplied)) {
sendVetoes();
}
}

public void setAllVetoes(List<VetoData> vetoes) {
public void updateVetoes(List<VetoData> vetoes) {
Map<VetoKey, Integer> vetosMap = vetoes.stream()
.collect(Collectors.toMap(
vetoData -> new VetoKey(vetoData.getMatchmakerQueueMapPoolId(),
vetoData.getMapPoolMapVersionId()),
VetoData::getVetoTokensApplied));
matchmakerPrefs.getAppliedVetoes().clear();
vetoes.forEach(v -> matchmakerPrefs.getAppliedVetoes().put(
new VetoKey(v.getMatchmakerQueueMapPoolId(), v.getMapPoolMapVersionId()),
v.getVetoTokensApplied()
));
matchmakerPrefs.getAppliedVetoes().putAll(vetosMap);
}

private void onVetoesChanged(VetoesChangedInfo vetoesChangedInfo) {
setAllVetoes(vetoesChangedInfo.getVetoes());
updateVetoes(vetoesChangedInfo.getVetoes());

if (vetoesChangedInfo.getForced()) {
notificationService.addNotification(
new ImmediateNotification(i18n.get("teammatchmaking.vetoes.forced.title"),
i18n.get("teammatchmaking.vetoes.forced.message"),
Severity.INFO,
Collections.singletonList(new DismissAction(i18n))));
notificationService.addNotification(new ImmediateNotification(i18n.get("teammatchmaking.vetoes.forced.title"),
i18n.get("teammatchmaking.vetoes.forced.message"),
Severity.INFO, Collections.singletonList(
new DismissAction(i18n))));
}
}

Expand All @@ -303,10 +308,8 @@ private List<VetoData> getVetoesAsList() {
.entrySet()
.stream()
.filter(entry -> entry.getValue() > 0)
.map(entry -> new VetoData(
entry.getKey().mapPoolMapVersionId(),
entry.getValue(),
entry.getKey().matchmakerQueueMapPoolId()))
.map(entry -> new VetoData(entry.getKey().mapPoolMapVersionId(), entry.getValue(),
entry.getKey().matchmakerQueueMapPoolId()))
.collect(Collectors.toList());
}

Expand Down Expand Up @@ -435,8 +438,7 @@ private Mono<MatchmakerQueueInfo> getQueueFromApi(MatchmakerInfo.MatchmakerQueue
.collection()
.setFilter(qBuilder().string("technicalName")
.eq(matchmakerQueue.getName()));
return fafApiAccessor.getMany(navigator)
.next().map(matchmakerMapper::map)
return fafApiAccessor.getMany(navigator).next().map(matchmakerMapper::map)
.map(queue -> matchmakerMapper.update(matchmakerQueue, queue))
.doOnNext(queue -> queue.setSelected(
!matchmakerPrefs.getUnselectedQueueIds().contains(queue.getId())))
Expand Down Expand Up @@ -464,20 +466,21 @@ public CompletableFuture<Boolean> joinQueues() {

return featuredModService.updateFeaturedModToLatest(FAF.getTechnicalName(), false)
.thenCompose(aVoid -> validQueues.stream()
.map(this::joinQueue)
.reduce((future1, future2) -> future1.thenCombine(future2,
(result1, result2) -> result1 || result2))
.orElse(CompletableFuture.completedFuture(false)))
.map(this::joinQueue)
.reduce((future1, future2) -> future1.thenCombine(future2,
(result1, result2) -> result1 || result2))
.orElse(CompletableFuture.completedFuture(false)))
.exceptionally(throwable -> {
throwable = ConcurrentUtil.unwrapIfCompletionException(throwable);
log.error("Unable to join queues", throwable);
if (throwable instanceof NotifiableException notifiableException) {
notificationService.addErrorNotification(notifiableException);
} else {
notificationService.addImmediateErrorNotification(throwable, "teammatchmaking.couldNotStart");
}
return false;
});
throwable = ConcurrentUtil.unwrapIfCompletionException(throwable);
log.error("Unable to join queues", throwable);
if (throwable instanceof NotifiableException notifiableException) {
notificationService.addErrorNotification(notifiableException);
} else {
notificationService.addImmediateErrorNotification(throwable,
"teammatchmaking.couldNotStart");
}
return false;
});
}

public void leaveQueues() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -638,10 +638,8 @@ public void testSendVetoesOnConnection() {
public void testSendVetoesOnVetoesChange() {
connectionState.set(ConnectionState.CONNECTED);
when(fafServerAccessor.getConnectionState()).thenReturn(ConnectionState.CONNECTED);
// Clear invocations from connection state change
org.mockito.Mockito.clearInvocations(fafServerAccessor);

matchmakerPrefs.getAppliedVetoes().put(new VetoKey(1, 1), 1);
instance.setTokensForMap(new VetoKey(1, 1), 1);

verify(fafServerAccessor, times(1)).setPlayerVetoes(anyList());
}
Comment on lines 638 to 648
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix the test setup to avoid triggering sendVetoes twice.

The test fails because setPlayerVetoes is called twice:

  1. Line 639 sets connectionState to CONNECTED, triggering the connection state listener in TeamMatchmakingService (lines 242-247) which calls sendVetoes()
  2. Line 642 calls setTokensForMap, which also calls sendVetoes() when the value changes (line 281)

To test only the veto change behavior, clear mock invocations after setting up the connection state.

Apply this diff:

   @Test
   public void testSendVetoesOnVetoesChange() {
     connectionState.set(ConnectionState.CONNECTED);
     when(fafServerAccessor.getConnectionState()).thenReturn(ConnectionState.CONNECTED);
+    clearInvocations(fafServerAccessor);
 
     instance.setTokensForMap(new VetoKey(1, 1), 1);
 
     verify(fafServerAccessor, times(1)).setPlayerVetoes(anyList());
   }
🧰 Tools
🪛 GitHub Actions: Checks

[error] 644-644: Mockito TooManyActualInvocations: fafServerAccessor.setPlayerVetoes(...) called 2 times, but expected to be called 1 time in testSendVetoesOnVetoesChange.

🤖 Prompt for AI Agents
In
src/test/java/com/faforever/client/teammatchmaking/TeamMatchmakingServiceTest.java
around lines 638 to 645, the test setup triggers sendVetoes twice (once when
setting connection state to CONNECTED and again when calling setTokensForMap),
so clear the mock invocation history after initializing the connection state;
specifically, after setting connectionState and stubbing
fafServerAccessor.getConnectionState(), call
Mockito.clearInvocations(fafServerAccessor) (or reset/clearInvocations on the
relevant mock) before calling instance.setTokensForMap so that verify(...,
times(1)).setPlayerVetoes(...) only observes the veto change invocation.

Expand Down
Loading