Inspired by 2020's April Fools' 20w14infinite Snapshot, this mod brings endless randomly generated dimensions into Minecraft.
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}