Inspired by 2020's April Fools' 20w14infinite Snapshot, this mod brings endless randomly generated dimensions into Minecraft.
at master 230 lines 12 kB view raw
1package net.lerariemann.infinity.util.config; 2 3import net.lerariemann.infinity.access.MinecraftServerAccess; 4import net.lerariemann.infinity.mixin.var.SoundListMixin; 5import net.lerariemann.infinity.registry.payload.c2s.JukeboxesC2SPayload; 6import net.lerariemann.infinity.util.InfinityMethods; 7import net.lerariemann.infinity.util.VersionMethods; 8import net.lerariemann.infinity.util.core.CommonIO; 9import net.lerariemann.infinity.util.core.NbtUtils; 10import net.lerariemann.infinity.util.core.RandomProvider; 11import net.lerariemann.infinity.util.platform.InfinityPlatform; 12import net.minecraft.client.Minecraft; 13import net.minecraft.nbt.CompoundTag; 14import net.minecraft.nbt.ListTag; 15import net.minecraft.nbt.StringTag; 16import net.minecraft.nbt.Tag; 17import net.minecraft.resources.ResourceLocation; 18import net.minecraft.server.MinecraftServer; 19import net.minecraft.server.packs.resources.Resource; 20//? if >1.21 { 21import net.minecraft.world.item.JukeboxSong; 22import net.minecraft.core.registries.Registries; 23import net.lerariemann.infinity.util.core.ConfigType; 24import net.lerariemann.infinity.InfinityMod; 25import net.lerariemann.infinity.util.loading.DimensionGrabber; 26 //?} 27import java.nio.file.Path; 28import net.minecraft.world.level.storage.LevelResource; 29import org.jspecify.annotations.NonNull; 30 31import java.io.IOException; 32import java.nio.ByteBuffer; 33import java.nio.ByteOrder; 34import java.nio.file.Files; 35import java.util.*; 36import java.util.stream.Stream; 37 38/** 39 * Pipeline for generating custom sound events and jukebox song definitions for every music track in the game. 40 */ 41public record SoundScanner(Map<ResourceLocation, Resource> soundIds) { 42 /** Holds a map which allows to get the list of all sound IDs in existence and .ogg data for each. 43 * Seeded by {@link SoundListMixin} on client launch. */ 44 public static SoundScanner instance; 45 public static final String lockFile = (InfinityPlatform.version > 20 ? "client" : "server") + "_sound_pack_data.json"; 46 public static boolean isPreloaded() { 47 return instance != null; 48 } 49 public static Stream<ResourceLocation> getMatchingLoadedIds() { 50 if (isPreloaded()) return instance.soundIds.keySet().stream().filter(s -> s.getPath().contains("music") && !s.getPath().contains("record")); 51 return Stream.of(); 52 } 53 54 /** 55 * <p>On player connect, the server sends a {@link net.lerariemann.infinity.registry.payload.s2c.SoundPackS2CPayload} payload to the client. 56 * <p>If the server already contains data upon which the client should create its resource pack, it holds this data. 57 * <p>Otherwise, this payload is empty, and the server relies on the client to create it and send it back to the server for future use. */ 58 public static void unpackDownloadedPack(CompoundTag songIds, Minecraft cl) { 59 //the client unpacks a non-empty payload only when needed, meaning only if it doesn't have necessary files yet 60 if (!songIds.isEmpty() && !Files.exists(cl.getResourcePackDirectory().resolve("infinity/assets/infinity/sounds.json"))) { 61 cl.execute(() -> saveResourcePack(cl, NbtUtils.getList(songIds, "entries", Tag.TAG_STRING).stream() 62 .map(NbtUtils::getAsString).map(VersionMethods::id), false)); 63 } 64 else if (isPreloaded()) { 65 cl.execute(() -> { 66 CompoundTag jukeboxes = saveResourcePack(cl, getMatchingLoadedIds(), true); 67 CompoundTag res = new CompoundTag(); 68 ListTag songIdsList = new ListTag(); 69 getMatchingLoadedIds().forEach(id -> songIdsList.add(StringTag.valueOf(id.toString()))); 70 res.put("entries", songIdsList); 71 res.put("jukeboxes", jukeboxes); 72 new JukeboxesC2SPayload(res).send(); 73 }); 74 } 75 } 76 /** 77 * Generating and saving a resource pack from a stream of identifiers that correspond to music tracks. 78 * @param sendJukeboxes if this is true, the method also generates and returns data that needs to be sent to the server 79 * to seed it with corresponding jukebox song definitions.*/ 80 public static CompoundTag saveResourcePack(Minecraft client, Stream<ResourceLocation> songIds, boolean sendJukeboxes) { 81 CompoundTag soundsForRP = new CompoundTag(); 82 CompoundTag subtitlesForRP = new CompoundTag(); 83 CompoundTag jukeboxes = new CompoundTag(); 84 songIds.forEach(id -> { 85 String str = id.toString().replace(".ogg", "").replace("sounds/", ""); 86 List<String> arr = Arrays.stream(str.split("[:/]")).toList(); //preloading IDs 87 String songID = "disc." + arr.get(0) + "." + arr.get(arr.size() - 1); 88 String subtitleID = InfinityPlatform.version > 21 ? makeSubtitle(str) : "infinity:subtitles." + songID; 89 String subtitleData = InfinityMethods.formatAsTitleCase(arr.get(0) + " - " + arr.get(arr.size() - 1)); 90 91 ListTag soundForRPList = new ListTag(); 92 CompoundTag soundForRPCompound = new CompoundTag(); 93 soundForRPCompound.putString("name", str); 94 soundForRPCompound.putBoolean("stream", true); 95 soundForRPList.add(soundForRPCompound); 96 CompoundTag soundForRP = new CompoundTag(); 97 soundForRP.put("sounds", soundForRPList); 98 soundForRP.putString("subtitle", subtitleID); 99 subtitlesForRP.putString(subtitleID, subtitleData); 100 soundsForRP.put(songID, soundForRP); 101 102 if (sendJukeboxes) { 103 if (!isPreloaded()) return; 104 double length; 105 try { 106 length = calculateDuration(instance.soundIds.get(id).open().readAllBytes()); 107 } catch (IOException e) { 108 length = 600; 109 } 110 jukeboxes.put(arr.get(arr.size() - 1), getJukeboxDef(songID, subtitleID, length)); 111 } 112 }); 113 114 Path dir = client.getResourcePackDirectory().resolve("infinity/assets/infinity"); 115 String filename = "sounds.json"; 116 String oldContents = CommonIO.readAsString(dir.resolve(filename).toFile()); 117 CommonIO.write(soundsForRP, dir, filename); 118 String newContents = CommonIO.readAsString(dir.resolve(filename).toFile()); 119 if (InfinityPlatform.version <= 21) 120 CommonIO.write(subtitlesForRP, client.getResourcePackDirectory().resolve("infinity/assets/infinity/lang"), "en_us.json"); 121 if (!Objects.equals(oldContents, newContents)) client.reloadResourcePacks(); 122 return jukeboxes; 123 } 124 125 public static String makeSubtitle(String str) { 126 String res = str.replace(':', '.').replace('/', '.'); 127 if (res.startsWith("minecraft.")) res = res.substring("minecraft.".length()); 128 return res; 129 } 130 131 /** Receiver for a C2S {@link JukeboxesC2SPayload} payload, which holds data to send to clients in the future for them to 132 * generate custom sound resource packs, as well as jukebox song definitions corresponding to this data. */ 133 public static void unpackUploadedJukeboxes(MinecraftServer server, CompoundTag data) { 134 if (!RandomProvider.rule("useSoundSyncPackets")) return; 135 if (!data.contains("jukeboxes") || !data.contains("entries")) return; 136 if (Files.exists(server.getWorldPath(LevelResource.DATAPACK_DIR).resolve(lockFile))) return; 137 138 //? if >1.21 { 139 CompoundTag allJukeboxes = NbtUtils.getCompound(data, "jukeboxes"); 140 Path pathJukeboxes = server.getWorldPath(LevelResource.DATAPACK_DIR).resolve("infinity/data/infinity/jukebox_song"); 141 for (String key : NbtUtils.keys(allJukeboxes)) { 142 if (allJukeboxes.get(key) instanceof CompoundTag jukebox) { 143 CommonIO.write(jukebox, pathJukeboxes, key + ".json"); 144 } 145 } 146 grabJukeboxes(server); 147 //?} else { 148 /*((MinecraftServerAccess)server).infinity$setJukeboxDataMap(data.getCompound("jukeboxes")); 149 CommonIO.write(data.getCompound("jukeboxes"), server.getWorldPath(LevelResource.DATAPACK_DIR), "server_sound_pack_data.json"); 150 *///?} 151 152 data.remove("jukeboxes"); 153 CompoundTag packData = new CompoundTag(); 154 packData.put("entries", NbtUtils.getList(data, "entries", Tag.TAG_STRING)); 155 CommonIO.write(packData, server.getWorldPath(LevelResource.DATAPACK_DIR), "client_sound_pack_data.json"); 156 } 157 158 /** Injects freshly received jukebox song definitions into the server's registries, config files and {@link RandomProvider}. */ 159 //? if >1.21 { 160 public static void grabJukeboxes(MinecraftServer server) { 161 Path pathJukeboxes = server.getWorldPath(LevelResource.DATAPACK_DIR).resolve("infinity/data/infinity/jukebox_song"); 162 InfinityMod.LOGGER.info("grabbing jukeboxes"); 163 DimensionGrabber.readCategoryFromDisk(server, JukeboxSong.DIRECT_CODEC, Registries.JUKEBOX_SONG, pathJukeboxes); 164 if (!((MinecraftServerAccess)server).infinity$needsInvocation()) { 165 ConfigFactory.of(VersionMethods.getRegistry(server.registryAccess(), Registries.JUKEBOX_SONG)).generate(ConfigType.JUKEBOXES); 166 InfinityMod.updateProvider(server); 167 } 168 } 169 //?} 170 171 /** 172 Getting a duration of an OGG file from its byte data; <a href="https://stackoverflow.com/a/44407355">implementation from StackOverflow</a>. 173 */ 174 public static double calculateDuration(byte[] track) throws IOException { 175 int rate = -1; 176 int length = -1; 177 int size = track.length; 178 179 for (int i = size-1-8-2-4; i>=0 && length<0; i--) { //4 bytes for "OggS", 2 unused bytes, 8 bytes for length 180 // Looking for length (value after last "OggS") 181 if ( 182 track[i]==(byte)'O' 183 && track[i+1]==(byte)'g' 184 && track[i+2]==(byte)'g' 185 && track[i+3]==(byte)'S' 186 ) { 187 byte[] byteArray = new byte[]{track[i+6],track[i+7],track[i+8],track[i+9],track[i+10],track[i+11],track[i+12],track[i+13]}; 188 ByteBuffer bb = ByteBuffer.wrap(byteArray); 189 bb.order(ByteOrder.LITTLE_ENDIAN); 190 length = bb.getInt(0); 191 } 192 } 193 for (int i = 0; i<size-8-2-4 && rate<0; i++) { 194 // Looking for rate (first value after "vorbis") 195 if ( 196 track[i]==(byte)'v' 197 && track[i+1]==(byte)'o' 198 && track[i+2]==(byte)'r' 199 && track[i+3]==(byte)'b' 200 && track[i+4]==(byte)'i' 201 && track[i+5]==(byte)'s' 202 ) { 203 byte[] byteArray = new byte[]{track[i+11],track[i+12],track[i+13],track[i+14]}; 204 ByteBuffer bb = ByteBuffer.wrap(byteArray); 205 bb.order(ByteOrder.LITTLE_ENDIAN); 206 rate = bb.getInt(0); 207 } 208 } 209 return length / (double) rate; 210 } 211 212 private static @NonNull CompoundTag getJukeboxDef(String songID, String subtitleID, double length) { 213 CompoundTag jukebox_def = new CompoundTag(); 214 //? if >1.21 { 215 CompoundTag sound_event = new CompoundTag(); 216 sound_event.putString("sound_id", "infinity:" + songID); 217 jukebox_def.put("sound_event", sound_event); 218 CompoundTag description = new CompoundTag(); 219 description.putString("translate", subtitleID); 220 jukebox_def.put("description", description); 221 jukebox_def.putFloat("length_in_seconds", (float)length); 222 jukebox_def.putInt("comparator_output", 15); 223 //?} else { 224 /*jukebox_def.putString("sound_id", songID); 225 jukebox_def.putString("sound_sub", subtitleID); 226 jukebox_def.putInt("sound_duration", (int)(20*length)); 227 *///?} 228 return jukebox_def; 229 } 230}