diff --git a/gradle.properties b/gradle.properties index 3155589..931844a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ loader_version=0.19.2 loom_version=1.15-SNAPSHOT # Mod Properties -mod_version=1.0.0 +mod_version=1.0.1 maven_group=skybakery archives_base_name=skybakery diff --git a/src/client/java/skybakery/SkyBakeryClient.java b/src/client/java/skybakery/SkyBakeryClient.java index 5641ecd..d3cdcdc 100644 --- a/src/client/java/skybakery/SkyBakeryClient.java +++ b/src/client/java/skybakery/SkyBakeryClient.java @@ -1,10 +1,65 @@ package skybakery; +import java.util.Optional; + import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.minecraft.client.network.ServerInfo; +import net.minecraft.item.ItemStack; +import skybakery.assets.NewYearCakeAsset; +import skybakery.repository.NewYearCakeRepository; +import skybakery.utilities.ChatMenu; public class SkyBakeryClient implements ClientModInitializer { + public static final String HYPIXEL_URL = "hypixel.net"; + public static final NewYearCakeRepository CAKE_REPOSITORY = new NewYearCakeRepository(); + @Override public void onInitializeClient() { - // This entrypoint is suitable for setting up client-specific logic, such as rendering. + ClientPlayConnectionEvents.JOIN.register((handler, sender, client) -> { + client.execute(() -> { + if (client.player == null) + return; + + ServerInfo server = client.getCurrentServerEntry(); + if (server == null || !server.address.contains(HYPIXEL_URL)) + return; + + if (CAKE_REPOSITORY.isEmpty()) { + CAKE_REPOSITORY.load(); + ChatMenu.send("Cake collection loaded."); + } + }); + }); + + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> { + ServerInfo server = client.getCurrentServerEntry(); + if (server == null || !server.address.contains(HYPIXEL_URL)) + return; + + CAKE_REPOSITORY.save(); + }); + + ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> { + if (client.player == null) + return; + + for (int i = 0; i < client.player.getInventory().size(); i++) { + ItemStack stack = client.player.getInventory().getStack(i); + if (stack.isEmpty()) + continue; + if (!stack.getName().getString().contains("New Year Cake")) + continue; + + Optional newYearCakeAsset = NewYearCakeAsset.from(stack); + + if (newYearCakeAsset.isEmpty()) { + ChatMenu.send("ERROR: Could not parse as NewYearCakeAsset"); + } else if (CAKE_REPOSITORY.add(newYearCakeAsset.get())) { + ChatMenu.send("Detected new New Year Cake! Adding to collection " + stack.getName().getString()); + } + } + }); } -} \ No newline at end of file +} diff --git a/src/client/java/skybakery/assets/BaseAsset.java b/src/client/java/skybakery/assets/BaseAsset.java new file mode 100644 index 0000000..4df04bc --- /dev/null +++ b/src/client/java/skybakery/assets/BaseAsset.java @@ -0,0 +1,12 @@ +package skybakery.assets; + +import java.time.Instant; + +public record BaseAsset( + String version, + String id, + String profileId, + long pricePaid, + Instant dateObtained, + ObtainedFrom obtainedFrom) { +} diff --git a/src/client/java/skybakery/assets/NewYearCakeAsset.java b/src/client/java/skybakery/assets/NewYearCakeAsset.java new file mode 100644 index 0000000..fd52939 --- /dev/null +++ b/src/client/java/skybakery/assets/NewYearCakeAsset.java @@ -0,0 +1,41 @@ +package skybakery.assets; + +import java.time.Instant; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import net.minecraft.component.DataComponentTypes; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NbtCompound; + +public record NewYearCakeAsset(BaseAsset base, String uuid, int year) { + + private static final String ITEM_ID = "NEW_YEAR_CAKE"; + private static final Pattern YEAR_PATTERN = Pattern.compile("Year (\\d+)"); + + public static Optional from(ItemStack stack) { + if (stack.isEmpty()) + return Optional.empty(); + + String name = stack.getName().getString(); + Matcher matcher = YEAR_PATTERN.matcher(name); + if (!matcher.find()) + return Optional.empty(); + + int year = Integer.parseInt(matcher.group(1)); + + var customData = stack.get(DataComponentTypes.CUSTOM_DATA); + String uuid = ""; + String itemId = ITEM_ID; + + if (customData != null) { + NbtCompound root = customData.copyNbt(); + uuid = root.getString("uuid").orElse(""); + itemId = root.getString("id").orElse(ITEM_ID); + } + + BaseAsset base = new BaseAsset("1", itemId, "", 0, Instant.now(), ObtainedFrom.UNKNOWN); + return Optional.of(new NewYearCakeAsset(base, uuid, year)); + } +} diff --git a/src/client/java/skybakery/assets/ObtainedFrom.java b/src/client/java/skybakery/assets/ObtainedFrom.java new file mode 100644 index 0000000..5b2b878 --- /dev/null +++ b/src/client/java/skybakery/assets/ObtainedFrom.java @@ -0,0 +1,7 @@ +package skybakery.assets; + +public enum ObtainedFrom { + AUCTION, + TRADE, + UNKNOWN +} diff --git a/src/client/java/skybakery/repository/NewYearCakeRepository.java b/src/client/java/skybakery/repository/NewYearCakeRepository.java new file mode 100644 index 0000000..d066805 --- /dev/null +++ b/src/client/java/skybakery/repository/NewYearCakeRepository.java @@ -0,0 +1,84 @@ +package skybakery.repository; + +import com.google.gson.*; +import com.google.gson.reflect.TypeToken; +import net.fabricmc.loader.api.FabricLoader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import skybakery.assets.NewYearCakeAsset; + +import java.io.*; +import java.lang.reflect.Type; +import java.nio.file.*; +import java.time.Instant; +import java.util.*; + +public class NewYearCakeRepository { + + private static final Logger LOGGER = LoggerFactory.getLogger(NewYearCakeRepository.class); + + private static final Gson GSON = new GsonBuilder() + .setPrettyPrinting() + .registerTypeAdapter(Instant.class, + (JsonSerializer) (src, type, ctx) -> new JsonPrimitive(src.toEpochMilli())) + .registerTypeAdapter(Instant.class, + (JsonDeserializer) (json, type, ctx) -> Instant.ofEpochMilli(json.getAsLong())) + .create(); + + private static final Type MAP_TYPE = new TypeToken>>() { + }.getType(); + + private final Path file; + private Map> cakes = new HashMap<>(); + + public NewYearCakeRepository() { + this.file = FabricLoader.getInstance().getConfigDir().resolve("skybakery/cakes.json"); + } + + public boolean add(NewYearCakeAsset cake) { + List forYear = cakes.computeIfAbsent(cake.year(), k -> new ArrayList<>()); + boolean alreadyExists = forYear.stream().anyMatch(c -> c.uuid().equals(cake.uuid())); + if (alreadyExists) return false; + forYear.add(cake); + return true; + } + + public List getByYear(int year) { + return Collections.unmodifiableList(cakes.getOrDefault(year, Collections.emptyList())); + } + + public Map> getAll() { + return Collections.unmodifiableMap(cakes); + } + + public Set getYears() { + return Collections.unmodifiableSet(cakes.keySet()); + } + + public boolean isEmpty() { + return cakes.isEmpty(); + } + + public void load() { + if (!Files.exists(file)) + return; + try (Reader reader = Files.newBufferedReader(file)) { + Map> loaded = GSON.fromJson(reader, MAP_TYPE); + if (loaded != null) + cakes = loaded; + } catch (IOException e) { + LOGGER.error("Failed to load cake collection from {}", file, e); + } + } + + public void save() { + try { + Files.createDirectories(file.getParent()); + try (Writer writer = Files.newBufferedWriter(file)) { + GSON.toJson(cakes, MAP_TYPE, writer); + } + } catch (IOException e) { + LOGGER.error("Failed to save cake collection to {}", file, e); + } + } +} diff --git a/src/client/java/skybakery/utilities/ChatMenu.java b/src/client/java/skybakery/utilities/ChatMenu.java new file mode 100644 index 0000000..a3273ae --- /dev/null +++ b/src/client/java/skybakery/utilities/ChatMenu.java @@ -0,0 +1,16 @@ +package skybakery.utilities; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.text.Text; + +public class ChatMenu { + private static final String PREFIX = "§d[SkyBakery]§r "; + + public static void send(String message) { + MinecraftClient client = MinecraftClient.getInstance(); + if (client.player == null) + return; + + client.player.sendMessage(Text.literal(PREFIX + message), false); + } +}