Compare commits

...

2 Commits

Author SHA1 Message Date
burkkyy 55714816bd display missing cakes and highlight cakes that are missing
build / build (push) Has been cancelled
2026-05-31 19:59:41 -07:00
burkkyy 0178eacd76 store cakes I have
build / build (push) Has been cancelled
2026-05-31 19:09:41 -07:00
9 changed files with 312 additions and 4 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ loader_version=0.19.2
loom_version=1.15-SNAPSHOT loom_version=1.15-SNAPSHOT
# Mod Properties # Mod Properties
mod_version=1.0.0 mod_version=1.0.1
maven_group=skybakery maven_group=skybakery
archives_base_name=skybakery archives_base_name=skybakery
+125 -2
View File
@@ -1,10 +1,133 @@
package skybakery; package skybakery;
import java.util.List;
import java.util.Optional;
import net.fabricmc.api.ClientModInitializer; import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
import net.minecraft.client.network.ServerInfo;
import net.minecraft.item.ItemStack;
import net.minecraft.screen.slot.Slot;
import skybakery.assets.NewYearCakeAsset;
import skybakery.mixin.client.HandledScreenAccessor;
import skybakery.repository.NewYearCakeRepository;
import skybakery.utilities.ChatMenu;
public class SkyBakeryClient implements ClientModInitializer { public class SkyBakeryClient implements ClientModInitializer {
public static final String HYPIXEL_URL = "hypixel.net";
public static final NewYearCakeRepository CAKE_REPOSITORY = new NewYearCakeRepository();
private static final int MAX_CAKE_YEAR = 500;
private static String formatRanges(List<Integer> years) {
StringBuilder sb = new StringBuilder();
int start = years.get(0);
int end = start;
for (int i = 1; i < years.size(); i++) {
if (years.get(i) == end + 1) {
end = years.get(i);
} else {
sb.append(start == end ? start : start + "-" + end).append(", ");
start = end = years.get(i);
}
}
sb.append(start == end ? start : start + "-" + end);
return sb.toString();
}
@Override @Override
public void onInitializeClient() { 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();
});
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> {
dispatcher.register(
ClientCommandManager.literal("skybakery")
.then(ClientCommandManager.literal("missing")
.executes(ctx -> {
List<Integer> missing = CAKE_REPOSITORY.getMissingYears(MAX_CAKE_YEAR);
if (missing.isEmpty()) {
ChatMenu.send("You have all cakes from Year 1 to " + MAX_CAKE_YEAR + "!");
} else {
ChatMenu.send("Missing " + missing.size() + "/" + MAX_CAKE_YEAR + " cakes: " + formatRanges(missing));
}
return 1;
}))
.then(ClientCommandManager.literal("flush")
.executes(ctx -> {
CAKE_REPOSITORY.save();
ChatMenu.send("Cake collection flushed to disk.");
return 1;
}))
);
});
ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> {
if (!(screen instanceof HandledScreen<?> handledScreen)) return;
if (!screen.getTitle().getString().contains("Auction")) return;
ScreenEvents.afterRender(screen).register((s, drawContext, mouseX, mouseY, tickDelta) -> {
HandledScreenAccessor accessor = (HandledScreenAccessor) handledScreen;
for (Slot slot : handledScreen.getScreenHandler().slots) {
ItemStack stack = slot.getStack();
if (stack.isEmpty()) continue;
if (!stack.getName().getString().contains("New Year Cake")) continue;
Optional<NewYearCakeAsset> asset = NewYearCakeAsset.from(stack);
if (asset.isEmpty()) continue;
if (CAKE_REPOSITORY.getByYear(asset.get().year()).isEmpty()) {
int x = accessor.getGuiLeft() + slot.x;
int y = accessor.getGuiTop() + slot.y;
drawContext.fill(x, y, x + 16, y + 16, 0x80FF0000);
}
}
});
});
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 = 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());
}
}
});
} }
} }
@@ -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) {
}
@@ -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<NewYearCakeAsset> 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));
}
}
@@ -0,0 +1,7 @@
package skybakery.assets;
public enum ObtainedFrom {
AUCTION,
TRADE,
UNKNOWN
}
@@ -0,0 +1,14 @@
package skybakery.mixin.client;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(HandledScreen.class)
public interface HandledScreenAccessor {
@Accessor("x")
int getGuiLeft();
@Accessor("y")
int getGuiTop();
}
@@ -0,0 +1,94 @@
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<Instant>) (src, type, ctx) -> new JsonPrimitive(src.toEpochMilli()))
.registerTypeAdapter(Instant.class,
(JsonDeserializer<Instant>) (json, type, ctx) -> Instant.ofEpochMilli(json.getAsLong()))
.create();
private static final Type MAP_TYPE = new TypeToken<Map<Integer, List<NewYearCakeAsset>>>() {
}.getType();
private final Path file;
private Map<Integer, List<NewYearCakeAsset>> cakes = new HashMap<>();
public NewYearCakeRepository() {
this.file = FabricLoader.getInstance().getConfigDir().resolve("skybakery/cakes.json");
}
public boolean add(NewYearCakeAsset cake) {
List<NewYearCakeAsset> 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<NewYearCakeAsset> getByYear(int year) {
return Collections.unmodifiableList(cakes.getOrDefault(year, Collections.emptyList()));
}
public Map<Integer, List<NewYearCakeAsset>> getAll() {
return Collections.unmodifiableMap(cakes);
}
public Set<Integer> getYears() {
return Collections.unmodifiableSet(cakes.keySet());
}
public boolean isEmpty() {
return cakes.isEmpty();
}
public List<Integer> getMissingYears(int maxYear) {
List<Integer> missing = new ArrayList<>();
for (int year = 1; year <= maxYear; year++) {
List<NewYearCakeAsset> forYear = cakes.get(year);
if (forYear == null || forYear.isEmpty())
missing.add(year);
}
return missing;
}
public void load() {
if (!Files.exists(file))
return;
try (Reader reader = Files.newBufferedReader(file)) {
Map<Integer, List<NewYearCakeAsset>> 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);
}
}
}
@@ -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);
}
}
@@ -3,7 +3,8 @@
"package": "skybakery.mixin.client", "package": "skybakery.mixin.client",
"compatibilityLevel": "JAVA_21", "compatibilityLevel": "JAVA_21",
"client": [ "client": [
"ExampleClientMixin" "ExampleClientMixin",
"HandledScreenAccessor"
], ],
"injectors": { "injectors": {
"defaultRequire": 1 "defaultRequire": 1