diff --git a/src/main/java/org/mvplugins/multiverse/inventories/MultiverseInventories.java b/src/main/java/org/mvplugins/multiverse/inventories/MultiverseInventories.java index e17226b4..e41b691e 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/MultiverseInventories.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/MultiverseInventories.java @@ -3,10 +3,8 @@ import com.dumptruckman.minecraft.util.Logging; import org.bukkit.entity.Player; import org.bukkit.plugin.PluginManager; -import org.mvplugins.multiverse.core.MultiverseCoreApi; import org.mvplugins.multiverse.core.config.CoreConfig; import org.mvplugins.multiverse.core.destination.DestinationsProvider; -import org.mvplugins.multiverse.core.inject.PluginServiceLocatorFactory; import org.mvplugins.multiverse.core.module.MultiverseModule; import org.mvplugins.multiverse.core.utils.StringFormatter; import org.mvplugins.multiverse.inventories.command.MVInvCommandConditions; @@ -21,6 +19,7 @@ import org.mvplugins.multiverse.inventories.handleshare.SingleShareWriter; import org.mvplugins.multiverse.inventories.handleshare.SpawnChangeListener; import org.mvplugins.multiverse.inventories.handleshare.WriteOnlyShareHandler; +import org.mvplugins.multiverse.inventories.profile.PlayerNamesMapper; import org.mvplugins.multiverse.inventories.profile.ProfileCacheManager; import org.mvplugins.multiverse.inventories.profile.ProfileDataSource; import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey; @@ -31,8 +30,6 @@ import org.mvplugins.multiverse.inventories.util.ItemStackConverter; import org.mvplugins.multiverse.inventories.util.Perm; import org.bukkit.Bukkit; -import org.mvplugins.multiverse.core.command.MVCommandManager; -import org.mvplugins.multiverse.core.inject.PluginServiceLocator; import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.jakarta.inject.Provider; import org.jvnet.hk2.annotations.Service; @@ -61,6 +58,8 @@ public class MultiverseInventories extends MultiverseModule { @Inject private Provider worldGroupManager; @Inject + private Provider playerNamesMapperProvider; + @Inject private Provider profileDataSource; @Inject private Provider profileCacheManager; @@ -119,6 +118,7 @@ public final void onEnable() { // Init other extensions this.hookLuckPerms(); this.dupingPatch = InventoriesDupingPatch.enableDupingPatch(this); + this.playerNamesMapperProvider.get().loadMap(); Logging.config("Version %s (API v%s) Enabled - By %s", this.getDescription().getVersion(), getVersionAsNumber(), StringFormatter.joinAnd(this.getDescription().getAuthors())); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandCompletion.java b/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandCompletion.java index ba25c35a..5ea0aadf 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandCompletion.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandCompletion.java @@ -5,18 +5,23 @@ import org.mvplugins.multiverse.core.command.MVCommandCompletions; import org.mvplugins.multiverse.core.command.MVCommandManager; import org.mvplugins.multiverse.core.config.handle.PropertyModifyAction; +import org.mvplugins.multiverse.core.utils.StringFormatter; import org.mvplugins.multiverse.external.acf.commands.BukkitCommandCompletionContext; import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.vavr.control.Try; import org.mvplugins.multiverse.inventories.config.InventoriesConfig; import org.mvplugins.multiverse.inventories.dataimport.DataImportManager; +import org.mvplugins.multiverse.inventories.profile.PlayerNamesMapper; import org.mvplugins.multiverse.inventories.profile.group.WorldGroup; import org.mvplugins.multiverse.inventories.profile.group.WorldGroupManager; +import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey; import org.mvplugins.multiverse.inventories.share.Sharables; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -28,21 +33,26 @@ public final class MVInvCommandCompletion { private final InventoriesConfig inventoriesConfig; private final WorldGroupManager worldGroupManager; private final DataImportManager dataImportManager; + private final PlayerNamesMapper playerNamesMapper; @Inject private MVInvCommandCompletion( @NotNull InventoriesConfig inventoriesConfig, @NotNull WorldGroupManager worldGroupManager, @NotNull DataImportManager dataImportManager, - @NotNull MVCommandManager mvCommandManager) { + @NotNull MVCommandManager mvCommandManager, + @NotNull PlayerNamesMapper playerNamesMapper + ) { this.inventoriesConfig = inventoriesConfig; this.worldGroupManager = worldGroupManager; this.dataImportManager = dataImportManager; + this.playerNamesMapper = playerNamesMapper; MVCommandCompletions commandCompletions = mvCommandManager.getCommandCompletions(); commandCompletions.registerAsyncCompletion("dataimporters", this::suggestDataImporters); commandCompletions.registerStaticCompletion("mvinvconfigs", inventoriesConfig.getStringPropertyHandle().getAllPropertyNames()); commandCompletions.registerAsyncCompletion("mvinvconfigvalues", this::suggestConfigValues); + commandCompletions.registerAsyncCompletion("mvinvplayernames", this::suggestPlayerNames); commandCompletions.registerAsyncCompletion("sharables", this::suggestSharables); commandCompletions.registerAsyncCompletion("shares", this::suggestShares); commandCompletions.registerAsyncCompletion("worldGroups", this::suggestWorldGroups); @@ -60,6 +70,25 @@ private Collection suggestConfigValues(BukkitCommandCompletionContext co .getOrElse(Collections.emptyList()); } + private Collection suggestPlayerNames(BukkitCommandCompletionContext context) { + if (Objects.equals(context.getInput(), "@all")) { + return Collections.emptyList(); + } + List playerNames = getPlayerNames(); + if (context.getInput().indexOf(',') == -1) { + playerNames.add("@all"); + return playerNames; + } + return StringFormatter.addonToCommaSeperated(context.getInput(), playerNames); + } + + private List getPlayerNames() { + return playerNamesMapper.getKeys() + .stream() + .map(GlobalProfileKey::getPlayerName) + .collect(Collectors.toList()); + } + private Collection suggestSharables(BukkitCommandCompletionContext context) { String scope = context.getConfig("scope", "enabled"); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandContexts.java b/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandContexts.java index 2d050826..763d9f25 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandContexts.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/command/MVInvCommandContexts.java @@ -4,12 +4,14 @@ import org.bukkit.Bukkit; import org.jvnet.hk2.annotations.Service; import org.mvplugins.multiverse.core.command.MVCommandManager; +import org.mvplugins.multiverse.core.utils.REPatterns; import org.mvplugins.multiverse.external.acf.commands.BukkitCommandExecutionContext; import org.mvplugins.multiverse.external.acf.commands.CommandContexts; import org.mvplugins.multiverse.external.acf.commands.InvalidCommandArgument; import org.mvplugins.multiverse.external.jakarta.inject.Inject; import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; import org.mvplugins.multiverse.external.vavr.control.Option; +import org.mvplugins.multiverse.inventories.profile.PlayerNamesMapper; import org.mvplugins.multiverse.inventories.profile.group.WorldGroup; import org.mvplugins.multiverse.inventories.profile.group.WorldGroupManager; import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey; @@ -25,10 +27,16 @@ public final class MVInvCommandContexts { private final WorldGroupManager worldGroupManager; + private final PlayerNamesMapper playerNamesMapper; @Inject - private MVInvCommandContexts(@NotNull MVCommandManager commandManager, @NotNull WorldGroupManager worldGroupManager) { + private MVInvCommandContexts( + @NotNull MVCommandManager commandManager, + @NotNull WorldGroupManager worldGroupManager, + @NotNull PlayerNamesMapper playerNamesMapper + ) { this.worldGroupManager = worldGroupManager; + this.playerNamesMapper = playerNamesMapper; CommandContexts commandContexts = commandManager.getCommandContexts(); commandContexts.registerContext(GlobalProfileKey[].class, this::parseGlobalProfileKeys); @@ -40,15 +48,14 @@ private MVInvCommandContexts(@NotNull MVCommandManager commandManager, @NotNull private GlobalProfileKey[] parseGlobalProfileKeys(BukkitCommandExecutionContext context) { String profileStrings = context.popFirstArg(); if (profileStrings.equals("@all")) { - return Arrays.stream(Bukkit.getOfflinePlayers()) - .map(GlobalProfileKey::create) - .toArray(GlobalProfileKey[]::new); + return playerNamesMapper.getKeys().toArray(GlobalProfileKey[]::new); } - String[] profileNames = profileStrings.split(","); + String[] profileNames = REPatterns.COMMA.split(profileStrings); return Arrays.stream(profileNames) - .map(Bukkit::getOfflinePlayer) - .map(GlobalProfileKey::create) + .map(playerNamesMapper::getKey) + .filter(Option::isDefined) + .map(Option::get) .toArray(GlobalProfileKey[]::new); } diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/globalprofile/ClearCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/globalprofile/ClearCommand.java index deaec4a0..24c8a73c 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/globalprofile/ClearCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/globalprofile/ClearCommand.java @@ -42,14 +42,15 @@ final class ClearCommand extends InventoriesCommand { @Subcommand("bulkedit globalprofile clear") @CommandPermission("multiverse.inventories.bulkedit") - @CommandCompletion("@players @flags:groupName=" + Flags.NAME) - @Syntax("") + @CommandCompletion("@mvinvplayernames @flags:groupName=" + Flags.NAME) + @Syntax(" [--clear-all-playerprofiles]") void onCommand( MVCommandIssuer issuer, @Syntax("") GlobalProfileKey[] globalProfileKeys, + @Syntax("[--clear-all-playerprofiles]") String[] flagArray ) { ParsedCommandFlags parsedFlags = flags.parse(flagArray); diff --git a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/globalprofile/ModifyCommand.java b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/globalprofile/ModifyCommand.java index 8ef3cc4e..bd419200 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/globalprofile/ModifyCommand.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/commands/bulkedit/globalprofile/ModifyCommand.java @@ -6,6 +6,7 @@ import org.mvplugins.multiverse.core.command.queue.CommandQueuePayload; import org.mvplugins.multiverse.core.config.handle.PropertyModifyAction; import org.mvplugins.multiverse.core.locale.message.Message; +import org.mvplugins.multiverse.external.acf.commands.annotation.CommandCompletion; import org.mvplugins.multiverse.external.acf.commands.annotation.CommandPermission; import org.mvplugins.multiverse.external.acf.commands.annotation.Subcommand; import org.mvplugins.multiverse.external.acf.commands.annotation.Syntax; @@ -32,6 +33,7 @@ final class ModifyCommand extends InventoriesCommand { @Subcommand("bulkedit globalprofile modify") @CommandPermission("multiverse.inventories.bulkedit") + @CommandCompletion("load-on-login|last-world @empty @mvinvplayernames") @Syntax(" ") void onCommand( MVCommandIssuer issuer, diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/FlatFileProfileDataSource.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/FlatFileProfileDataSource.java index 0f508fad..f9afe7d5 100644 --- a/src/main/java/org/mvplugins/multiverse/inventories/profile/FlatFileProfileDataSource.java +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/FlatFileProfileDataSource.java @@ -34,16 +34,19 @@ final class FlatFileProfileDataSource implements ProfileDataSource { private final AsyncFileIO asyncFileIO; private final ProfileFilesLocator profileFilesLocator; private final ProfileCacheManager profileCacheManager; + private final PlayerNamesMapper playerNamesMapper; @Inject FlatFileProfileDataSource( @NotNull AsyncFileIO asyncFileIO, @NotNull ProfileFilesLocator profileFilesLocator, - @NotNull ProfileCacheManager profileCacheManager + @NotNull ProfileCacheManager profileCacheManager, + @NotNull PlayerNamesMapper playerNamesMapper ) { this.asyncFileIO = asyncFileIO; this.profileFilesLocator = profileFilesLocator; this.profileCacheManager = profileCacheManager; + this.playerNamesMapper = playerNamesMapper; } private FileConfiguration loadFileToJsonConfiguration(File file) { @@ -263,7 +266,7 @@ public CompletableFuture getGlobalProfile(GlobalProfileKey key) { globalProfile.setLastKnownName(key.getPlayerName()); return CompletableFuture.completedFuture(globalProfile); } - return asyncFileIO.queueFileCallable(globalFile, () -> getGlobalProfileFromDisk(key.getPlayerUUID(), key.getPlayerName(), globalFile)); + return asyncFileIO.queueFileCallable(globalFile, () -> getGlobalProfileFromDisk(key.getPlayerUUID(), globalFile)); }); } @@ -289,10 +292,8 @@ private void migrateGlobalProfileToUUID(UUID playerUUID, String playerName) { } } - private GlobalProfile getGlobalProfileFromDisk(UUID playerUUID, String playerName, File globalFile) { - GlobalProfile globalProfile = new GlobalProfile(playerUUID, globalFile.toPath()); - globalProfile.setLastKnownName(playerName); - return globalProfile; + private GlobalProfile getGlobalProfileFromDisk(UUID playerUUID, File globalFile) { + return new GlobalProfile(playerUUID, globalFile.toPath()); } /** @@ -314,7 +315,11 @@ private CompletableFuture modifyGlobalProfile(GlobalProfile globalProfile, @Override public CompletableFuture updateGlobalProfile(GlobalProfile globalProfile) { File globalFile = profileFilesLocator.getGlobalFile(globalProfile.getPlayerUUID().toString()); - return asyncFileIO.queueFileAction(globalFile, () -> processGlobalProfileWrite(globalProfile)); + boolean didPlayerNameChange = playerNamesMapper.setPlayerName(globalProfile.getPlayerUUID(), globalProfile.getLastKnownName()); + return asyncFileIO.queueFileAction(globalFile, () -> processGlobalProfileWrite(globalProfile)) + .thenCompose(ignore -> didPlayerNameChange + ? asyncFileIO.queueFileAction(playerNamesMapper.getFile(), playerNamesMapper::savePlayerNames) + : CompletableFuture.completedFuture(null)); } private void processGlobalProfileWrite(GlobalProfile globalProfile) { diff --git a/src/main/java/org/mvplugins/multiverse/inventories/profile/PlayerNamesMapper.java b/src/main/java/org/mvplugins/multiverse/inventories/profile/PlayerNamesMapper.java new file mode 100644 index 00000000..c316ef16 --- /dev/null +++ b/src/main/java/org/mvplugins/multiverse/inventories/profile/PlayerNamesMapper.java @@ -0,0 +1,149 @@ +package org.mvplugins.multiverse.inventories.profile; + +import com.dumptruckman.minecraft.util.Logging; +import com.google.common.base.Strings; +import net.minidev.json.JSONObject; +import net.minidev.json.JSONValue; +import net.minidev.json.parser.JSONParser; +import org.jvnet.hk2.annotations.Service; +import org.mvplugins.multiverse.external.jakarta.inject.Inject; +import org.mvplugins.multiverse.external.jakarta.inject.Provider; +import org.mvplugins.multiverse.external.jetbrains.annotations.NotNull; +import org.mvplugins.multiverse.external.vavr.control.Option; +import org.mvplugins.multiverse.inventories.MultiverseInventories; +import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +@Service +public final class PlayerNamesMapper { + + private static final String FILENAME = "playernames.json"; + + private final Provider profileDataSourceProvider; + private final Provider profileCacheManagerProvider; + + private final File playerNamesFile; + private final Map playerNamesMap; + private final Map playerUUIDMap; + + private Map playerNamesJson; + + @Inject + PlayerNamesMapper( + @NotNull MultiverseInventories inventories, + @NotNull Provider profileDataSourceProvider, + @NotNull Provider profileCacheManagerProvider + ) { + this.profileDataSourceProvider = profileDataSourceProvider; + this.profileCacheManagerProvider = profileCacheManagerProvider; + + this.playerNamesFile = new File(inventories.getDataFolder(), FILENAME); + this.playerNamesMap = new ConcurrentHashMap<>(); + this.playerUUIDMap = new ConcurrentHashMap<>(); + } + + public void loadMap() { + Logging.config("Loading player names map..."); + playerNamesMap.clear(); + playerUUIDMap.clear(); + if (playerNamesFile.exists()) { + loadFromPlayerNamesFile(); + } else { + buildPlayerNamesMap(); + } + } + + private void loadFromPlayerNamesFile() { + try (FileReader fileReader = new FileReader(playerNamesFile)) { + playerNamesJson = new ConcurrentHashMap<>((JSONObject) new JSONParser(JSONParser.DEFAULT_PERMISSIVE_MODE).parse(fileReader)); + if (playerNamesJson.isEmpty()) { + buildPlayerNamesMap(); + return; + } + playerNamesJson.forEach((String uuid, Object name) -> { + UUID playerUUID = UUID.fromString(uuid); + String playerName = String.valueOf(name); + GlobalProfileKey globalProfileKey = GlobalProfileKey.create(playerUUID, playerName); + playerNamesMap.put(playerName, globalProfileKey); + playerUUIDMap.put(playerUUID, globalProfileKey); + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void buildPlayerNamesMap() { + Logging.info("Generating player names map... This may take a while."); + playerNamesJson = new ConcurrentHashMap<>(); + + ProfileDataSource profileDataSource = profileDataSourceProvider.get(); + CompletableFuture[] futures = profileDataSource.listGlobalProfileUUIDs().stream() + .map(uuid -> profileDataSource.getGlobalProfile(GlobalProfileKey.create(uuid)) + .thenAccept(globalProfile -> setPlayerName(uuid, globalProfile.getLastKnownName()))) + .toArray(CompletableFuture[]::new); + CompletableFuture.allOf(futures).thenRun(this::savePlayerNames).join(); + profileCacheManagerProvider.get().clearAllGlobalProfileCaches(); + Logging.info("Generated player names map."); + } + + boolean setPlayerName(UUID uuid, String name) { + if (playerNamesJson == null) { + throw new IllegalStateException("Player names mapper has not been loaded yet."); + } + if (Strings.isNullOrEmpty(name)) { + return false; + } + if (getKey(name).filter(g -> g.getPlayerUUID().equals(uuid)).isDefined()) { + return false; + } + + Logging.finer("Setting player name mapping for %s to %s", uuid, name); + GlobalProfileKey globalProfileKey = GlobalProfileKey.create(uuid, name); + + // Handle remove of old playername + Object oldName = playerNamesJson.put(uuid.toString(), name); + playerNamesMap.remove(String.valueOf(oldName)); + + playerNamesMap.put(name, globalProfileKey); + playerUUIDMap.put(uuid, globalProfileKey); + return true; + } + + void savePlayerNames() { + if (playerNamesJson == null) { + throw new IllegalStateException("Player names mapper has not been loaded yet."); + } + + Logging.finer("Saving player names map..."); + try (FileWriter fileWriter = new FileWriter(playerNamesFile)) { + fileWriter.write(JSONValue.toJSONString(playerNamesJson)); + } catch (Exception e) { + e.printStackTrace(); + } + Logging.finer("Saving player names map... Done!"); + } + + File getFile() { + return playerNamesFile; + } + + public Option getKey(String playerName) { + return Option.of(playerNamesMap.get(playerName)); + } + + public Option getKey(UUID playerUUID) { + return Option.of(playerUUIDMap.get(playerUUID)); + } + + public List getKeys() { + return playerNamesMap.values().stream().toList(); + } +} diff --git a/src/test/java/org/mvplugins/multiverse/inventories/profile/FilePerformanceTest.kt b/src/test/java/org/mvplugins/multiverse/inventories/profile/FilePerformanceTest.kt index e4e10b65..1ce4407f 100644 --- a/src/test/java/org/mvplugins/multiverse/inventories/profile/FilePerformanceTest.kt +++ b/src/test/java/org/mvplugins/multiverse/inventories/profile/FilePerformanceTest.kt @@ -48,10 +48,10 @@ class FilePerformanceTest : TestWithMockBukkit() { } @Test - fun `Test 10K global profiles`() { + fun `Test 1K global profiles`() { val startTime = System.nanoTime() - val futures = ArrayList>(10000) - for (i in 0..9999) { + val futures = ArrayList>(1000) + for (i in 0..1000) { futures.add(profileDataSource.modifyGlobalProfile(GlobalProfileKey.create(UUID.randomUUID()), { globalProfile -> globalProfile.setLoadOnLogin(true) })) diff --git a/src/test/java/org/mvplugins/multiverse/inventories/profile/PlayerNameChangeTest.kt b/src/test/java/org/mvplugins/multiverse/inventories/profile/PlayerNameChangeTest.kt index 4afaa294..115945ce 100644 --- a/src/test/java/org/mvplugins/multiverse/inventories/profile/PlayerNameChangeTest.kt +++ b/src/test/java/org/mvplugins/multiverse/inventories/profile/PlayerNameChangeTest.kt @@ -11,22 +11,20 @@ import org.mvplugins.multiverse.inventories.profile.group.WorldGroupManager import org.mvplugins.multiverse.inventories.profile.key.GlobalProfileKey import org.mvplugins.multiverse.inventories.util.FutureNow import java.nio.file.Path -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotEquals -import kotlin.test.assertTrue +import kotlin.test.* class PlayerNameChangeTest : TestWithMockBukkit() { private lateinit var profileDataSource: ProfileDataSource + private lateinit var playerNamesMapper: PlayerNamesMapper private lateinit var player: PlayerMock @BeforeTest fun setUp() { profileDataSource = serviceLocator.getService(ProfileDataSource::class.java).takeIf { it != null } ?: run { throw IllegalStateException("ProfileDataSource is not available as a service") } + playerNamesMapper = serviceLocator.getService(PlayerNamesMapper::class.java).takeIf { it != null } ?: run { + throw IllegalStateException("PlayerNamesMapper is not available as a service") } val worldManager = serviceLocator.getActiveService(WorldManager::class.java).takeIf { it != null } ?: run { throw IllegalStateException("WorldManager is not available as a service") } @@ -40,6 +38,7 @@ class PlayerNameChangeTest : TestWithMockBukkit() { assertTrue(worldGroupManager.load().isSuccess) player = server.addPlayer("Benji_0224") + assertEquals(GlobalProfileKey.create(player.uniqueId, "Benji_0224"), playerNamesMapper.getKey("Benji_0224").orNull) } @Test @@ -81,5 +80,9 @@ class PlayerNameChangeTest : TestWithMockBukkit() { // check player profile assertEquals("benthecat10", FutureNow.get(profileDataSource.getGlobalProfile(GlobalProfileKey.create(player)))?.lastKnownName) + + // check name mapper updated + assertEquals(GlobalProfileKey.create(player.uniqueId, "benthecat10"), playerNamesMapper.getKey("benthecat10").orNull) + assertNull(playerNamesMapper.getKey("Benji_0224").orNull) } }