Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Museum Item Cache #389

Merged
merged 4 commits into from
Oct 28, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,9 @@ dependencies {
modLocalRuntime "dev.emi:emi-fabric:${project.emi_version}"

// Renderer (https://github.com/0x3C50/Renderer)
include modImplementation("com.github.0x3C50:Renderer:${project.renderer_version}") {
exclude group: "io.github.ladysnake" exclude module: "satin"
}
//include modImplementation("com.github.0x3C50:Renderer:${project.renderer_version}") {
//exclude group: "io.github.ladysnake" exclude module: "satin"
//}
AzureAaron marked this conversation as resolved.
Show resolved Hide resolved

include modImplementation("meteordevelopment:discord-ipc:1.1")

Expand Down
1 change: 1 addition & 0 deletions src/main/java/de/hysky/skyblocker/SkyblockerMod.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ public void onInitializeClient() {
ItemProtection.init();
CreeperBeams.init();
ItemRarityBackgrounds.init();
MuseumItemCache.init();
containerSolverManager.init();
statusBarTracker.init();
Scheduler.INSTANCE.scheduleCyclic(Utils::update, 20);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package de.hysky.skyblocker.skyblock;

import com.mojang.blaze3d.systems.RenderSystem;
import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.item.PriceInfoTooltip;
import de.hysky.skyblocker.utils.ItemUtils;
Expand Down Expand Up @@ -103,13 +102,7 @@ private static void render(WorldRenderContext wrc, BlockHitResult blockHitResult
@SuppressWarnings("DataFlowIssue")
BlockState state = client.world.getBlockState(pos);
if (!state.isAir() && client.world.getBlockState(pos.up()).isAir() && client.world.getBlockState(pos.up(2)).isAir()) {
RenderSystem.polygonOffset(-1f, -10f);
RenderSystem.enablePolygonOffset();

RenderHelper.renderFilledIfVisible(wrc, pos, COLOR_COMPONENTS, 0.5f);

RenderSystem.polygonOffset(0f, 0f);
RenderSystem.disablePolygonOffset();
}
}
}
146 changes: 146 additions & 0 deletions src/main/java/de/hysky/skyblocker/skyblock/item/MuseumItemCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package de.hysky.skyblocker.skyblock.item;

import java.io.ByteArrayInputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import com.mojang.util.UndashedUuid;

import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.utils.Http;
import de.hysky.skyblocker.utils.Http.ApiResponse;
import de.hysky.skyblocker.utils.Utils;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.minecraft.client.MinecraftClient;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.nbt.NbtElement;
import net.minecraft.nbt.NbtIo;
import net.minecraft.nbt.NbtList;
import net.minecraft.util.Util;

public class MuseumItemCache {
private static final Logger LOGGER = LoggerFactory.getLogger(MuseumItemCache.class);
private static final Path CACHE_FILE = SkyblockerMod.CONFIG_DIR.resolve("museum_item_cache.json");
private static final Object2ObjectOpenHashMap<String, Object2ObjectOpenHashMap<String, ProfileMuseumData>> MUSEUM_ITEM_CACHE = new Object2ObjectOpenHashMap<>();
private static final Type MAP_TYPE = new TypeToken<Object2ObjectOpenHashMap<String, Object2ObjectOpenHashMap<String, ProfileMuseumData>>>() {}.getType();

private static CompletableFuture<Void> loaded;

public static void init() {
ClientLifecycleEvents.CLIENT_STARTED.register(MuseumItemCache::load);
}

private static void load(MinecraftClient client) {
loaded = CompletableFuture.runAsync(() -> {
try (BufferedReader reader = Files.newBufferedReader(CACHE_FILE)) {
Object2ObjectOpenHashMap<String, Object2ObjectOpenHashMap<String, ProfileMuseumData>> cachedData = SkyblockerMod.GSON.fromJson(reader, MAP_TYPE);

MUSEUM_ITEM_CACHE.putAll(cachedData);
LOGGER.info("[Skyblocker] Loaded museum items cache");
} catch (NoSuchFileException ignored) {
} catch (IOException e) {
LOGGER.error("[Skyblocker] Failed to load cached museum items", e);
}
});
}

private static void save() {
CompletableFuture.runAsync(() -> {
try (BufferedWriter writer = Files.newBufferedWriter(CACHE_FILE)) {
SkyblockerMod.GSON.toJson(MUSEUM_ITEM_CACHE, writer);
} catch (IOException e) {
LOGGER.error("[Skyblocker] Failed to save cached museum items!", e);
}
});
}

private static void updateData4ProfileMember(String uuid, String profileId) {
CompletableFuture.runAsync(() -> {
try {
ApiResponse response = Http.sendHypixelRequest("skyblock/museum", "?profile=" + profileId);

//The request was successful
if (response.ok()) {
JsonObject profileData = JsonParser.parseString(response.content()).getAsJsonObject();
JsonObject memberData = profileData.get("members").getAsJsonObject().get(uuid).getAsJsonObject();

//We call them sets because it could either be a singular item or an entire armour set
Map<String, JsonElement> donatedSets = memberData.get("items").getAsJsonObject().asMap();

//Set of all found item ids on profile
ObjectOpenHashSet<String> itemIds = new ObjectOpenHashSet<>();

for (Map.Entry<String, JsonElement> donatedSet : donatedSets.entrySet()) {
//Item is plural here because the nbt is a list
String itemsData = donatedSet.getValue().getAsJsonObject().get("items").getAsJsonObject().get("data").getAsString();
NbtList items = NbtIo.readCompressed(new ByteArrayInputStream(Base64.getDecoder().decode(itemsData))).getList("i", NbtElement.COMPOUND_TYPE);

for (int i = 0; i < items.size(); i++) {
NbtCompound tag = items.getCompound(i).getCompound("tag");

if (tag.contains("ExtraAttributes")) {
NbtCompound extraAttributes = tag.getCompound("ExtraAttributes");

if (extraAttributes.contains("id")) itemIds.add(extraAttributes.getString("id"));
}
}
}

MUSEUM_ITEM_CACHE.get(uuid).put(profileId, new ProfileMuseumData(System.currentTimeMillis(), itemIds));
save();

LOGGER.info("[Skyblocker] Successfully updated museum item cache for profile {}", profileId);
}
} catch (Exception e) {
LOGGER.error("[Skyblocker] Failed to refresh museum item data for profile {}", profileId, e);
}
});
}

/**
* The cache is ticked upon switching skyblock servers
*/
public static void tick(String profileId) {
if (loaded.isDone()) {
String uuid = UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull());
Object2ObjectOpenHashMap<String, ProfileMuseumData> playerData = MUSEUM_ITEM_CACHE.computeIfAbsent(uuid, uuid1 -> Util.make(new Object2ObjectOpenHashMap<>(), map -> {
map.put(profileId, ProfileMuseumData.EMPTY);
}));

if (playerData.get(profileId).stale()) updateData4ProfileMember(uuid, profileId);
}
}

public static boolean hasItemInMuseum(String id) {
String uuid = UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull());
ObjectOpenHashSet<String> collectedItemIds = MUSEUM_ITEM_CACHE.get(uuid).get(Utils.getProfileId()).collectedItemIds();

return collectedItemIds != null && collectedItemIds.contains(id);
}

private record ProfileMuseumData(long lastUpdated, ObjectOpenHashSet<String> collectedItemIds) {
private static final ProfileMuseumData EMPTY = new ProfileMuseumData(0L, null);
private static final long MAX_AGE = 86_400_000;

private boolean stale() {
return System.currentTimeMillis() > lastUpdated + MAX_AGE;
}
}
}
8 changes: 6 additions & 2 deletions src/main/java/de/hysky/skyblocker/utils/Http.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ private static ApiResponse sendCacheableGetRequest(String url) throws IOExceptio
InputStream decodedInputStream = getDecodedInputStream(response);
String body = new String(decodedInputStream.readAllBytes());

return new ApiResponse(body, getCacheStatus(response.headers()));
return new ApiResponse(body, response.statusCode(), getCacheStatus(response.headers()));
}

public static HttpHeaders sendHeadRequest(String url) throws IOException, InterruptedException {
Expand Down Expand Up @@ -120,7 +120,11 @@ public static String getCacheStatus(HttpHeaders headers) {
}

//TODO If ever needed, we could just replace cache status with the response headers and go from there
public record ApiResponse(String content, String cacheStatus) {
public record ApiResponse(String content, int statusCode, String cacheStatus) {

public boolean ok() {
return statusCode == 200;
}

public boolean cached() {
return cacheStatus.equals("HIT");
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/de/hysky/skyblocker/utils/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import de.hysky.skyblocker.events.SkyblockEvents;
import de.hysky.skyblocker.skyblock.item.MuseumItemCache;
import de.hysky.skyblocker.skyblock.item.PriceInfoTooltip;
import de.hysky.skyblocker.skyblock.rift.TheRift;
import de.hysky.skyblocker.utils.scheduler.MessageScheduler;
Expand Down Expand Up @@ -365,6 +366,8 @@ public static boolean onChatMessage(Text text, boolean overlay) {

if (isOnSkyblock && message.startsWith("Profile ID: ")) {
profileId = message.replace("Profile ID: ", "");

MuseumItemCache.tick(profileId);
}

return true;
Expand Down
42 changes: 32 additions & 10 deletions src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import de.hysky.skyblocker.utils.render.culling.OcclusionCulling;
import de.hysky.skyblocker.utils.render.title.Title;
import de.hysky.skyblocker.utils.render.title.TitleContainer;
import me.x150.renderer.render.Renderer3d;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.font.TextRenderer;
Expand All @@ -22,8 +21,6 @@
import org.joml.Matrix4f;
import org.lwjgl.opengl.GL11;

import java.awt.*;

public class RenderHelper {
private static final Vec3d ONE = new Vec3d(1, 1, 1);
private static final int MAX_OVERWORLD_BUILD_HEIGHT = 319;
Expand All @@ -36,20 +33,45 @@ public static void renderFilledThroughWallsWithBeaconBeam(WorldRenderContext con

public static void renderFilledThroughWalls(WorldRenderContext context, BlockPos pos, float[] colorComponents, float alpha) {
if (FrustumUtils.isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1)) {
Renderer3d.renderThroughWalls();
renderFilled(context, pos, colorComponents, alpha);
Renderer3d.stopRenderThroughWalls();
renderFilled(context, Vec3d.of(pos), ONE, colorComponents, alpha, true);
}
}

public static void renderFilledIfVisible(WorldRenderContext context, BlockPos pos, float[] colorComponents, float alpha) {
if (OcclusionCulling.isVisible(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1)) {
renderFilled(context, pos, colorComponents, alpha);
renderFilled(context, Vec3d.of(pos), ONE, colorComponents, alpha, false);
}
}

private static void renderFilled(WorldRenderContext context, BlockPos pos, float[] colorComponents, float alpha) {
Renderer3d.renderFilled(context.matrixStack(), new Color(colorComponents[0], colorComponents[1], colorComponents[2], alpha), Vec3d.of(pos), ONE);

private static void renderFilled(WorldRenderContext context, Vec3d pos, Vec3d dimensions, float[] colorComponents, float alpha, boolean throughWalls) {
MatrixStack matrices = context.matrixStack();
Vec3d camera = context.camera().getPos();
Tessellator tessellator = RenderSystem.renderThreadTesselator();
BufferBuilder buffer = tessellator.getBuffer();

matrices.push();
matrices.translate(-camera.x, -camera.y, -camera.z);

RenderSystem.setShader(GameRenderer::getPositionColorProgram);
RenderSystem.setShaderColor(1f, 1f, 1f, 1f);
RenderSystem.polygonOffset(-1f, -10f);
RenderSystem.enablePolygonOffset();
RenderSystem.enableBlend();
RenderSystem.defaultBlendFunc();
RenderSystem.enableDepthTest();
RenderSystem.depthFunc(throughWalls ? GL11.GL_ALWAYS : GL11.GL_LEQUAL);
RenderSystem.disableCull();

buffer.begin(DrawMode.TRIANGLE_STRIP, VertexFormats.POSITION_COLOR);
WorldRenderer.renderFilledBox(matrices, buffer, pos.x, pos.y, pos.z, pos.x + dimensions.x, pos.y + dimensions.y, pos.z + dimensions.z, colorComponents[0], colorComponents[1], colorComponents[2], alpha);
tessellator.draw();

matrices.pop();
RenderSystem.polygonOffset(0f, 0f);
RenderSystem.disablePolygonOffset();
RenderSystem.disableBlend();
RenderSystem.disableDepthTest();
RenderSystem.enableCull();
}

private static void renderBeaconBeam(WorldRenderContext context, BlockPos pos, float[] colorComponents) {
Expand Down