Last active
May 8, 2024 20:32
-
-
Save mjtb49/f3e01e3355178d2bb6c814606971c374 to your computer and use it in GitHub Desktop.
1.20-pre4 Loot Example
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
import com.google.common.base.Charsets; | |
import com.google.common.hash.HashFunction; | |
import com.google.common.hash.Hashing; | |
import com.google.common.primitives.Longs; | |
import java.util.Random; | |
public class LootTableRNG { | |
private static final HashFunction MD5 = Hashing.md5(); | |
private long seedLo; | |
private long seedHi; | |
private final long seedLoHash; | |
private final long seedHiHash; | |
public LootTableRNG(String identifier) { | |
byte[] bs = MD5.hashString(identifier, Charsets.UTF_8).asBytes(); | |
seedLoHash = Longs.fromBytes(bs[0], bs[1], bs[2], bs[3], bs[4], bs[5], bs[6], bs[7]); | |
seedHiHash = Longs.fromBytes(bs[8], bs[9], bs[10], bs[11], bs[12], bs[13], bs[14], bs[15]); | |
} | |
public void setSeed(long seed) { | |
long l_seed = seed ^ 0x6A09E667F3BCC909L; | |
this.seedLo = mixStafford13(l_seed) ^ seedLoHash; | |
this.seedHi = mixStafford13(l_seed + -7046029254386353131L) ^ seedHiHash; | |
if ((this.seedLo | this.seedHi) == 0L) { | |
this.seedLo = -7046029254386353131L; | |
this.seedHi = 7640891576956012809L; | |
} | |
//1.20-pre3 burns a nextLong() call | |
this.nextLong(); | |
} | |
public static long mixStafford13(long seed) { | |
seed = (seed ^ seed >>> 30) * -4658895280553007687L; | |
seed = (seed ^ seed >>> 27) * -7723592293110705685L; | |
return seed ^ seed >>> 31; | |
} | |
public long nextLong() { | |
long l = this.seedLo; | |
long m = this.seedHi; | |
//return value only | |
long n = Long.rotateLeft(l + m, 17) + l; | |
//actual generator | |
m ^= l; | |
this.seedLo = Long.rotateLeft(l, 49) ^ m ^ m << 21; | |
this.seedHi = Long.rotateLeft(m, 28); | |
return n; | |
} | |
private long nextBits(int i) { | |
return this.nextLong() >>> 64 - i; | |
} | |
public float nextFloat() { | |
return (float)this.nextBits(24) * 5.9604645E-8F; | |
} | |
public double nextDouble() { | |
return (double)this.nextBits(53) * 1.110223E-16F; | |
} | |
public int nextInt() { | |
return (int)this.nextLong(); | |
} | |
public int nextInt(int i) { | |
if (i <= 0) { | |
throw new IllegalArgumentException("Bound must be positive"); | |
} else { | |
long l = Integer.toUnsignedLong(this.nextInt()); | |
long m = l * (long)i; | |
long n = m & 4294967295L; | |
if (n < (long)i) { | |
for(int j = Integer.remainderUnsigned(~i + 1, i); n < (long)j; n = m & 4294967295L) { | |
l = Integer.toUnsignedLong(this.nextInt()); | |
m = l * (long)i; | |
} | |
} | |
long o = m >> 32; | |
return (int)o; | |
} | |
} | |
public static String toPercentage(float n, int digits){ | |
return String.format("%."+digits+"f",n*100); | |
} | |
public static boolean test_loot(LootTableRNG rng) { | |
//Rewrite this for each loot table. Be careful since loot tables might do extra calls besides what you target, | |
//which this method must do as well. That is, this method should imitate a full call to the loot table. | |
//You must look in the code to determine exactly the nature of the needed test, or experiment | |
//determines melon slice count | |
int val = rng.nextInt(5); | |
//melons have something extra in their loot table which does a call after the number of slices is chosen | |
//the next line accounts for that | |
rng.nextLong(); | |
return (val == 4); | |
} | |
public static void main(String[] args) { | |
int max = 0; | |
//Loot tables can be found by renaming the 1.20-pre2 jar to a .zip | |
//then navigating to /data/minecraft/loot_tables | |
//The needed string is at the bottom of a loot table | |
//You also must modify the test_loot function | |
LootTableRNG target = new LootTableRNG("minecraft:blocks/melon"); | |
for (long seed = 0;; seed++) { | |
target.setSeed(seed); | |
int count = 0; | |
while (test_loot(target)) { | |
count += 1; | |
} | |
if (count > max) { | |
max = count; | |
System.out.println("Found " + count); | |
System.out.println(seed); | |
} | |
} | |
//System.out.println("Done!"); | |
/* | |
//Code for correlation finding. Be very careful that the RNGs you target don't do extra calls - if they do | |
//correlations will still exist but you need to account for the calls when comparing. | |
int calls = 1000; | |
LootTableRNG rng1 = new LootTableRNG("minecraft:blocks/melon"); | |
int rng1_calls = 5; | |
LootTableRNG rng2 = new LootTableRNG("minecraft:entities/blaze"); | |
int rng2_calls = 2; | |
int[][][] heatmap = new int[calls][rng1_calls][rng2_calls]; | |
for(long attempt = 0; attempt < 100000; attempt++) { | |
long seed = new Random().nextLong(); | |
rng1.setSeed(seed); | |
rng2.setSeed(seed); | |
for (int call = 0; call < calls; call ++) { | |
heatmap[call][rng1.nextInt(rng1_calls)][rng2.nextInt(rng2_calls)] += 1; | |
//Burn a call because melons do (and blazes don't, so we need to kill blazes at twice the rate as the melons). | |
rng1.nextLong(); | |
rng2.nextLong(); | |
} | |
} | |
for (int call = 0; call < calls; call ++) { | |
boolean should_print = false; | |
for(int[] row: heatmap[call]) { | |
int sum = 0; | |
for (int i : row) { | |
sum += i; | |
} | |
for (int i : row) { | |
should_print |= Math.abs(i/(float) sum - 0.5) > 0.49; | |
} | |
} | |
if (should_print) { | |
System.out.println("Blaze " + (2 * call + 1) + " Melon " + (call + 1)); | |
for (int[] row : heatmap[call]) { | |
int sum = 0; | |
for (int i : row) { | |
sum += i; | |
} | |
for (int i : row) { | |
System.out.print(toPercentage(i / (float) sum, 2) + " "); | |
} | |
System.out.println(); | |
} | |
System.out.println(); | |
} | |
}*/ | |
} | |
} |
Note for versions 1.20-pre5 to latest (1.20.6): not only was the burn call removed in setSeed, as mentioned by jakiki6, but the xor was moved before the mixStafford13. The updated setSeed function should be:
public void setSeed(long seed) {
long l2 = seed ^ 0x6A09E667F3BCC909L;
long l3 = l2 + -7046029254386353131L;
// XOR happens before mixing in 1.20-pre5 and later
this.seedLo = mixStafford13(l2 ^ seedLoHash);
this.seedHi = mixStafford13(l3 ^ seedHiHash);
if ((this.seedLo | this.seedHi) == 0L) {
this.seedLo = -7046029254386353131L;
this.seedHi = 7640891576956012809L;
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
btw the burn call in setSeed was removed in 1.20pre5: