Last active
September 16, 2022 23:35
-
-
Save AlexTMjugador/f555b9822a769a12f3f85a608699c9c5 to your computer and use it in GitHub Desktop.
Quick and dirty Minecraft offline to online mode UUID migrator. Works for a Paper server with some plugins, including LuckPerms, when using its default H2 storage engine.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package io.github.alextmjugador; | |
import lombok.Data; | |
@Data | |
public class PlayerProfile { | |
private String name; | |
private String id; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<project xmlns="http://maven.apache.org/POM/4.0.0" | |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | |
<modelVersion>4.0.0</modelVersion> | |
<groupId>io.github.alextmjugador</groupId> | |
<artifactId>offline-uuid-migrator</artifactId> | |
<version>1.0-SNAPSHOT</version> | |
<name>offline-uuid-migrator</name> | |
<properties> | |
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |
<maven.compiler.source>17</maven.compiler.source> | |
<maven.compiler.target>17</maven.compiler.target> | |
</properties> | |
<dependencies> | |
<dependency> | |
<groupId>org.glassfish.jersey.core</groupId> | |
<artifactId>jersey-client</artifactId> | |
<version>3.1.0-M8</version> | |
</dependency> | |
<dependency> | |
<groupId>org.glassfish.jersey.inject</groupId> | |
<artifactId>jersey-hk2</artifactId> | |
<version>3.1.0-M8</version> | |
</dependency> | |
<dependency> | |
<groupId>org.glassfish.jersey.media</groupId> | |
<artifactId>jersey-media-json-binding</artifactId> | |
<version>3.1.0-M8</version> | |
</dependency> | |
<dependency> | |
<groupId>jakarta.activation</groupId> | |
<artifactId>jakarta.activation-api</artifactId> | |
<version>2.1.0</version> | |
</dependency> | |
<dependency> | |
<groupId>com.github.TheNullicorn</groupId> | |
<artifactId>Nedit</artifactId> | |
<version>2.1.1</version> | |
</dependency> | |
<dependency> | |
<groupId>org.xerial</groupId> | |
<artifactId>sqlite-jdbc</artifactId> | |
<version>3.39.3.0</version> | |
</dependency> | |
<dependency> | |
<groupId>com.h2database</groupId> | |
<artifactId>h2</artifactId> | |
<version>1.4.200</version> | |
</dependency> | |
<dependency> | |
<groupId>org.projectlombok</groupId> | |
<artifactId>lombok</artifactId> | |
<version>1.18.24</version> | |
<scope>provided</scope> | |
</dependency> | |
</dependencies> | |
<build> | |
<pluginManagement> | |
<plugins> | |
<plugin> | |
<artifactId>maven-clean-plugin</artifactId> | |
<version>3.2.0</version> | |
</plugin> | |
<plugin> | |
<artifactId>maven-resources-plugin</artifactId> | |
<version>3.3.0</version> | |
</plugin> | |
<plugin> | |
<artifactId>maven-shade-plugin</artifactId> | |
<version>3.3.0</version> | |
<configuration> | |
<createDependencyReducedPom>false</createDependencyReducedPom> | |
<transformers> | |
<transformer | |
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> | |
<mainClass>io.github.alextmjugador.UuidMigrator</mainClass> | |
</transformer> | |
<transformer | |
implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> | |
<resource>META-INF/services/java.sql.Driver</resource> | |
</transformer> | |
</transformers> | |
</configuration> | |
</plugin> | |
<plugin> | |
<artifactId>maven-compiler-plugin</artifactId> | |
<version>3.10.1</version> | |
</plugin> | |
<plugin> | |
<artifactId>maven-surefire-plugin</artifactId> | |
<version>3.0.0-M7</version> | |
</plugin> | |
<plugin> | |
<artifactId>maven-jar-plugin</artifactId> | |
<version>3.3.0</version> | |
</plugin> | |
<plugin> | |
<artifactId>maven-install-plugin</artifactId> | |
<version>3.0.1</version> | |
</plugin> | |
<plugin> | |
<artifactId>maven-deploy-plugin</artifactId> | |
<version>3.0.0</version> | |
</plugin> | |
<plugin> | |
<artifactId>maven-site-plugin</artifactId> | |
<version>3.12.1</version> | |
</plugin> | |
<plugin> | |
<artifactId>maven-project-info-reports-plugin</artifactId> | |
<version>3.4.1</version> | |
</plugin> | |
</plugins> | |
</pluginManagement> | |
<plugins> | |
<plugin> | |
<artifactId>maven-shade-plugin</artifactId> | |
<executions> | |
<execution> | |
<phase>package</phase> | |
<goals> | |
<goal>shade</goal> | |
</goals> | |
</execution> | |
</executions> | |
</plugin> | |
</plugins> | |
</build> | |
<repositories> | |
<repository> | |
<id>jitpack.io</id> | |
<url>https://jitpack.io</url> | |
</repository> | |
</repositories> | |
</project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package io.github.alextmjugador; | |
import java.io.File; | |
import java.io.FileInputStream; | |
import java.io.FileNotFoundException; | |
import java.io.FileOutputStream; | |
import java.io.IOException; | |
import java.nio.charset.StandardCharsets; | |
import java.nio.file.Files; | |
import java.nio.file.NoSuchFileException; | |
import java.nio.file.Path; | |
import java.nio.file.StandardCopyOption; | |
import java.sql.DriverManager; | |
import java.sql.SQLException; | |
import java.util.List; | |
import java.util.UUID; | |
import java.util.regex.Pattern; | |
import jakarta.ws.rs.ProcessingException; | |
import jakarta.ws.rs.client.ClientBuilder; | |
import jakarta.ws.rs.core.MediaType; | |
import jakarta.ws.rs.core.UriBuilder; | |
import me.nullicorn.nedit.NBTReader; | |
import me.nullicorn.nedit.NBTWriter; | |
import me.nullicorn.nedit.type.NBTCompound; | |
public class UuidMigrator { | |
private static final Pattern PLAYER_ID_PATTERN = Pattern | |
.compile("^([0-9a-fA-F]{8})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]{4})([0-9a-fA-F]{12})$"); | |
private static final String MAIN_WORLD_NAME = "Khron"; | |
public static void main(String[] args) { | |
cleanupWorldGuardUuidCache(); | |
final List<String> accountsToMigrate; | |
try { | |
accountsToMigrate = Files.lines(Path.of("accounts_to_migrate.txt"), StandardCharsets.UTF_8).toList(); | |
} catch (final IOException exc) { | |
exc.printStackTrace(); | |
System.exit(1); | |
return; | |
} | |
for (final var accountName : accountsToMigrate) { | |
final var offlineUuid = UUID | |
.nameUUIDFromBytes(("OfflinePlayer:" + accountName).getBytes(StandardCharsets.UTF_8)); | |
final PlayerProfile playerProfile; | |
try { | |
playerProfile = ClientBuilder.newClient() | |
.target(UriBuilder.fromUri("https://api.mojang.com/users/profiles/minecraft/{accountName}") | |
.build(accountName)) | |
.request(MediaType.APPLICATION_JSON) | |
.buildGet() | |
.invoke(PlayerProfile.class); | |
} catch (final ProcessingException exc) { | |
exc.printStackTrace(); | |
System.exit(2); | |
return; | |
} | |
System.out.print("- Processing player " + accountName); | |
if (playerProfile != null) { | |
final var onlineUuid = UUID | |
.fromString(PLAYER_ID_PATTERN.matcher(playerProfile.getId()).replaceFirst("$1-$2-$3-$4-$5")); | |
System.out.println(" (offline UUID: " + offlineUuid + " -> online UUID: " + onlineUuid + ")..."); | |
replaceUuidLinesIn(Path.of("whitelist.json"), offlineUuid, onlineUuid); | |
migrateUuidNamedFileAtDirectory(Path.of(MAIN_WORLD_NAME, "advancements"), offlineUuid, onlineUuid); | |
migrateUuidNamedFileAtDirectory(Path.of(MAIN_WORLD_NAME, "stats"), offlineUuid, onlineUuid); | |
migratePlayerDataAtDirectory(Path.of(MAIN_WORLD_NAME, "playerdata"), offlineUuid, onlineUuid); | |
migrateBlockBallPlayerData(offlineUuid, onlineUuid); | |
migrateChatcolor2PlayerData(offlineUuid, onlineUuid); | |
migrateLuckPermsData(offlineUuid, onlineUuid); | |
migrateUuidNamedFileAtDirectory(Path.of("plugins", "WorldEdit", "sessions"), offlineUuid, onlineUuid); | |
migrateWorldGuardRegionData(offlineUuid, onlineUuid); | |
System.out.println("OK"); | |
} else { | |
System.out.println("... not a Mojang account, skipping"); | |
} | |
} | |
} | |
private static void replaceUuidLinesIn(final Path filePath, final UUID oldUuid, final UUID newUuid) { | |
final Pattern uuidLinePattern = Pattern.compile("^(\\s*)\"uuid\": *\"" + oldUuid + "\" *,$"); | |
final List<String> newLines; | |
try { | |
newLines = Files.lines(filePath, StandardCharsets.UTF_8) | |
.map((final String line) -> uuidLinePattern.matcher(line).replaceFirst("$1\"uuid\": \"" + newUuid + "\",")) | |
.toList(); | |
} catch (final IOException exc) { | |
exc.printStackTrace(); | |
System.exit(1); | |
return; | |
} | |
try { | |
Files.write(filePath, newLines, StandardCharsets.UTF_8); | |
} catch (final IOException exc) { | |
exc.printStackTrace(); | |
System.exit(1); | |
return; | |
} | |
System.out.println("Migrated UUID in " + filePath); | |
} | |
private static void migrateUuidNamedFileAtDirectory(final Path directory, final UUID oldUuid, final UUID newUuid) { | |
final var oldUuidFilePath = directory.resolve(oldUuid + ".json"); | |
final var newUuidFilePath = directory.resolve(newUuid + ".json"); | |
try { | |
Files.move( | |
oldUuidFilePath, | |
newUuidFilePath, | |
StandardCopyOption.REPLACE_EXISTING | |
); | |
System.out.println("Migrated " + oldUuidFilePath + " to " + newUuidFilePath); | |
} catch (final NoSuchFileException exc) { | |
// Ignore, nothing to migrate | |
} catch (final IOException exc) { | |
exc.printStackTrace(); | |
System.exit(1); | |
} | |
} | |
private static void migratePlayerDataAtDirectory(final Path directory, final UUID oldUuid, final UUID newUuid) { | |
try { | |
// Migrate unread offline player data | |
final var oldUuidPlayerDataPath = directory.resolve(oldUuid + ".dat"); | |
try { | |
final NBTCompound playerData; | |
try (final var inputStream = new FileInputStream(oldUuidPlayerDataPath.toFile())) { | |
playerData = NBTReader.read(inputStream); | |
} | |
final var uuidInts = playerData.getIntArray("UUID"); | |
if (uuidInts != null) { | |
final var newUuidMostSigBits = newUuid.getMostSignificantBits(); | |
for (int i = 0; i < 2; ++i) { | |
uuidInts[i] = (int) (newUuidMostSigBits >> 32 * (1 - i)); | |
} | |
final var newUuidLeastSigBits = newUuid.getLeastSignificantBits(); | |
for (int i = 0; i < 2; ++i) { | |
uuidInts[i + 2] = (int) (newUuidLeastSigBits >> 32 * (1 - i)); | |
} | |
try (final var outputStream = new FileOutputStream(directory.resolve(newUuid + ".dat").toFile())) { | |
NBTWriter.write(playerData, outputStream); | |
} | |
Files.deleteIfExists(oldUuidPlayerDataPath); | |
} | |
System.out.println("Migrated offline player data"); | |
} catch (final FileNotFoundException exc) { | |
System.out.println("No offline player data at " + oldUuidPlayerDataPath); | |
} | |
// Clean up offline player data migrated by the server | |
if (Files.deleteIfExists(directory.resolve(oldUuid + ".dat.offline-read"))) { | |
System.out.println("Cleaned up read offline player data"); | |
} | |
} catch (final IOException exc) { | |
exc.printStackTrace(); | |
System.exit(1); | |
} | |
} | |
private static void migrateBlockBallPlayerData(final UUID oldUuid, final UUID newUuid) { | |
try (final var dbConnection = DriverManager.getConnection( | |
"jdbc:sqlite:" + Path.of("plugins", "BlockBall", "BlockBall.db") | |
)) { | |
final var statement = dbConnection.prepareStatement( | |
"UPDATE OR IGNORE 'SHY_PLAYER' SET uuid = ? WHERE uuid = ?" | |
); | |
statement.setString(1, newUuid.toString()); | |
statement.setString(2, oldUuid.toString()); | |
statement.execute(); | |
System.out.println("Migrated BlockBall stats"); | |
} catch (final SQLException exc) { | |
exc.printStackTrace(); | |
System.exit(3); | |
} | |
} | |
private static void migrateChatcolor2PlayerData(final UUID oldUuid, final UUID newUuid) { | |
final Pattern uuidLinePattern = Pattern.compile("^(.*): " + oldUuid + "$"); | |
final var playerListFile = Path.of("plugins", "ChatColor2", "player-list.yml"); | |
final var playerSettingsDirectory = Path.of("plugins", "ChatColor2", "players"); | |
try { | |
final var newPlayerListLines = Files.lines(playerListFile, StandardCharsets.UTF_8) | |
.map((final String line) -> uuidLinePattern.matcher(line).replaceFirst("$1: " + newUuid)) | |
.toList(); | |
Files.write(playerListFile, newPlayerListLines, StandardCharsets.UTF_8); | |
try { | |
Files.move( | |
playerSettingsDirectory.resolve(oldUuid + ".yml"), | |
playerSettingsDirectory.resolve(newUuid + ".yml"), | |
StandardCopyOption.REPLACE_EXISTING | |
); | |
} catch (final NoSuchFileException exc) { | |
// Ignore, nothing to migrate | |
} | |
System.out.println("Migrated ChatColor2 settings"); | |
} catch (final IOException exc) { | |
exc.printStackTrace(); | |
System.exit(1); | |
} | |
} | |
private static void migrateLuckPermsData(final UUID oldUuid, final UUID newUuid) { | |
try (final var dbConnection = DriverManager.getConnection( | |
"jdbc:h2:." + File.separator + Path.of("plugins", "LuckPerms", "luckperms-h2") | |
)) { | |
dbConnection.setAutoCommit(false); | |
for (final var updateData : new String[][] { | |
new String[] { "LUCKPERMS_USER_PERMISSIONS", "UUID" }, | |
new String[] { "LUCKPERMS_PLAYERS", "UUID" }, | |
new String[] { "LUCKPERMS_ACTIONS", "ACTOR_UUID" }, | |
new String[] { "LUCKPERMS_ACTIONS", "ACTED_UUID" } | |
}) { | |
final var statement = dbConnection.prepareStatement( | |
"UPDATE " + updateData[0] + " SET " + updateData[1] + " = ? WHERE " + updateData[1] + " = ?" | |
); | |
statement.setString(1, newUuid.toString()); | |
statement.setString(2, oldUuid.toString()); | |
statement.execute(); | |
} | |
dbConnection.commit(); | |
System.out.println("Migrated LuckPerms data"); | |
} catch (final SQLException exc) { | |
exc.printStackTrace(); | |
System.exit(3); | |
} | |
} | |
private static void migrateWorldGuardRegionData(final UUID oldUuid, final UUID newUuid) { | |
try { | |
for (final File worldGuardWorldDirectory : | |
Path.of("plugins", "WorldGuard", "worlds").toFile().listFiles( | |
(final File childFile) -> childFile.isDirectory() | |
) | |
) { | |
final var regionsFilePath = worldGuardWorldDirectory.toPath().resolve("regions.yml"); | |
try { | |
final var newLines = Files | |
.lines(regionsFilePath, StandardCharsets.UTF_8) | |
.map((final String line) -> line.replace(oldUuid.toString(), newUuid.toString())) | |
.toList(); | |
Files.write(regionsFilePath, newLines, StandardCharsets.UTF_8); | |
} catch (final NoSuchFileException exc) { | |
// Ignore, no region data for this world | |
} | |
} | |
System.out.println("Migrated WorldGuard region data"); | |
} catch (final IOException exc) { | |
exc.printStackTrace(); | |
System.exit(1); | |
} | |
} | |
private static void cleanupWorldGuardUuidCache() { | |
try { | |
Files.deleteIfExists(Path.of("plugins", "WorldGuard", "profiles.sqlite")); | |
Files.deleteIfExists(Path.of("plugins", "WorldGuard", "cache", "profiles.sqlite")); | |
System.out.println("Cleaned up WorldGuard UUID cache"); | |
} catch (final IOException exc) { | |
// Ignored | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment