Skip to content

Commit 671b059

Browse files
committed
feat(users): offline UUD generation support
1 parent b3e2087 commit 671b059

File tree

2 files changed

+80
-0
lines changed

2 files changed

+80
-0
lines changed

src/main/java/me/itzg/helpers/users/ManageUsersCommand.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.util.HashSet;
1515
import java.util.List;
1616
import java.util.Set;
17+
import java.util.UUID;
1718
import java.util.concurrent.Callable;
1819
import lombok.extern.slf4j.Slf4j;
1920
import me.itzg.helpers.errors.GenericException;
@@ -25,7 +26,10 @@
2526
import me.itzg.helpers.json.ObjectMappers;
2627
import me.itzg.helpers.users.model.JavaOp;
2728
import me.itzg.helpers.users.model.JavaUser;
29+
30+
import org.apache.commons.codec.digest.DigestUtils;
2831
import org.apache.maven.artifact.versioning.ComparableVersion;
32+
2933
import picocli.CommandLine.ArgGroup;
3034
import picocli.CommandLine.Command;
3135
import picocli.CommandLine.ExitCode;
@@ -46,6 +50,9 @@ public class ManageUsersCommand implements Callable<Integer> {
4650
@Option(names = {"--help", "-h"}, usageHelp = true)
4751
boolean help;
4852

53+
@Option(names = {"--offline"}, required = false, description = "Use for offline server, UUIDs are generated")
54+
boolean offline;
55+
4956
@Option(names = "--output-directory", defaultValue = ".")
5057
Path outputDirectory;
5158

@@ -240,6 +247,10 @@ private JavaUser resolveJavaUserId(SharedFetch sharedFetch, List<? extends JavaU
240247
}
241248
}
242249

250+
if (offline) {
251+
return getOfflineUUID(input);
252+
}
253+
243254
final UserApi userApi;
244255
switch (userApiProvider) {
245256
case mojang:
@@ -329,4 +340,32 @@ private void processInputAsFile(SharedFetch sharedFetch, String filePathUrl) thr
329340
private boolean usesTextUserList() {
330341
return version != null && new ComparableVersion(version).compareTo(MIN_VERSION_USES_JSON) < 0;
331342
}
343+
344+
private static JavaUser getOfflineUUID(String username) {
345+
byte[] bytes = DigestUtils.md5("OfflinePlayer:"+username);
346+
347+
// Force version = 3 (bits 12-15 of time_hi_and_version)
348+
bytes[6] &= 0x0F;
349+
bytes[6] |= 0x30;
350+
351+
// Force variant = 2 (bits 6-7 of clock_seq_hi_and_reserved)
352+
bytes[8] &= 0x3F;
353+
bytes[8] |= 0x80;
354+
355+
long msb = 0;
356+
long lsb = 0;
357+
358+
for (int i = 0; i < 8; i++) {
359+
msb = (msb << 8) | (bytes[i] & 0xFF);
360+
}
361+
362+
for (int i = 8; i < 16; i++) {
363+
lsb = (lsb << 8) | (bytes[i] & 0xFF);
364+
}
365+
366+
return JavaUser.builder()
367+
.name(username)
368+
.uuid(new UUID(msb, lsb).toString())
369+
.build();
370+
}
332371
}

src/test/java/me/itzg/helpers/users/ManageUsersCommandTest.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ class ManageUsersCommandTest {
2525

2626
private static final String USER1_ID = "3f5f20286a85445fa7b46100e70c2b3a";
2727
private static final String USER1_UUID = "3f5f2028-6a85-445f-a7b4-6100e70c2b3a";
28+
private static final String USER1_OFFLINE_UUID = "fb4cdad9-642b-358f-8f6f-717981c9f42b";
2829
private static final String USER2_ID = "5e5a1b2294b14f5892466062597e4c91";
2930
private static final String USER2_UUID = "5e5a1b22-94b1-4f58-9246-6062597e4c91";
31+
private static final String USER2_OFFLINE_UUID = "6e7d9aa0-0da2-390c-ab6a-377df9d77518";
3032

3133
@TempDir
3234
Path tempDir;
@@ -406,6 +408,45 @@ void givenUuidsAndAllExist(WireMockRuntimeInfo wmInfo) throws IOException {
406408
}
407409
}
408410

411+
@Nested
412+
public class whitelistOffline {
413+
@Test
414+
void givenNames(WireMockRuntimeInfo wmInfo) {
415+
setupUserStubs();
416+
417+
final int exitCode = new CommandLine(
418+
new ManageUsersCommand()
419+
)
420+
.execute(
421+
"--mojang-api-base-url", wmInfo.getHttpBaseUrl(),
422+
"--user-api-provider", "mojang",
423+
"--offline",
424+
"--type", "JAVA_WHITELIST",
425+
"--output-directory", tempDir.toString(),
426+
"user1", "user2"
427+
);
428+
429+
assertThat(exitCode).isEqualTo(0);
430+
431+
final Path expectedFile = tempDir.resolve("whitelist.json");
432+
433+
assertThat(expectedFile).exists();
434+
435+
assertJson(expectedFile)
436+
.isArrayContainingExactlyInAnyOrder(
437+
conditions()
438+
.satisfies(conditions()
439+
.at("/name").hasValue("user1")
440+
.at("/uuid").hasValue(USER1_OFFLINE_UUID)
441+
)
442+
.satisfies(conditions()
443+
.at("/name").hasValue("user2")
444+
.at("/uuid").hasValue(USER2_OFFLINE_UUID)
445+
)
446+
);
447+
}
448+
}
449+
409450
@Nested
410451
public class whitelistOrOpsText {
411452

0 commit comments

Comments
 (0)