Skip to content

Instantly share code, notes, and snippets.

@AlexTMjugador
Last active September 16, 2022 23:35
Show Gist options
  • Save AlexTMjugador/f555b9822a769a12f3f85a608699c9c5 to your computer and use it in GitHub Desktop.
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.
package io.github.alextmjugador;
import lombok.Data;
@Data
public class PlayerProfile {
private String name;
private String id;
}
<?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>
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