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

Add raytracing to the egg finder to make it fairer #845

Merged
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ dependencies {

// Apache Commons Math
include implementation("org.apache.commons:commons-math3:${project.commons_math_version}")

// Apache Commons Text
include implementation("org.apache.commons:commons-text:${project.commons_text_version}")
}

loom {
Expand Down
2 changes: 2 additions & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ repoparser_version = 1.5.0
jgit_version = 6.9.0.202403050737-r
## Apache Commons Math (https://mvnrepository.com/artifact/org.apache.commons/commons-math3)
commons_math_version = 3.6.1
## Apache Commons Text (https://mvnrepository.com/artifact/org.apache.commons/commons-text)
commons_text_version = 1.12.0

# Mod Properties
mod_version = 1.21.1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import de.hysky.skyblocker.utils.waypoint.Waypoint;
import it.unimi.dsi.fastutil.objects.ObjectImmutableList;
import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext;
Expand All @@ -24,7 +25,7 @@
import net.minecraft.text.HoverEvent;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import org.apache.commons.lang3.text.WordUtils;
import org.apache.commons.text.WordUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -39,18 +40,40 @@ public class EggFinder {
private static final Pattern eggFoundPattern = Pattern.compile("^(?:HOPPITY'S HUNT You found a Chocolate|You have already collected this Chocolate) (Breakfast|Lunch|Dinner)");
private static final Pattern newEggPattern = Pattern.compile("^HOPPITY'S HUNT A Chocolate (Breakfast|Lunch|Dinner) Egg has appeared!$");
private static final Logger logger = LoggerFactory.getLogger("Skyblocker Egg Finder");
//This is most likely unnecessary with the addition of the location change packet, but it works fine and might be doing something so might as well keep it
private static final LinkedList<ArmorStandEntity> armorStandQueue = new LinkedList<>();
/**
* The locations that the egg finder should work while the player is in.
*/
private static final Location[] possibleLocations = {Location.CRIMSON_ISLE, Location.CRYSTAL_HOLLOWS, Location.DUNGEON_HUB, Location.DWARVEN_MINES, Location.HUB, Location.THE_END, Location.THE_PARK, Location.GOLD_MINE, Location.DEEP_CAVERNS, Location.SPIDERS_DEN, Location.THE_FARMING_ISLAND};
/**
* Whether the player is in a location where the egg finder should work.
* This is set to false upon world change and will be checked with the location change event afterward.
*/
private static boolean isLocationCorrect = false;

private EggFinder() {
}
private EggFinder() {}

public static void init() {
ClientPlayConnectionEvents.JOIN.register((ignored, ignored2, ignored3) -> invalidateState());
ClientPlayConnectionEvents.JOIN.register((ignored, ignored2, ignored3) -> isLocationCorrect = false);
SkyblockEvents.LOCATION_CHANGE.register(EggFinder::handleLocationChange);
ClientReceiveMessageEvents.GAME.register(EggFinder::onChatMessage);
WorldRenderEvents.AFTER_TRANSLUCENT.register(EggFinder::renderWaypoints);
ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableEggFinder || client.player == null) return;
if (!isLocationCorrect || SkyblockTime.skyblockSeason.get() != SkyblockTime.Season.SPRING) return;
for (EggType type : EggType.entries) {
AzureAaron marked this conversation as resolved.
Show resolved Hide resolved
Egg egg = type.egg;
if (egg != null && !egg.seen && client.player.canSee(egg.entity)) {
type.setSeen();
}
}
});
SkyblockTime.HOUR_CHANGE.register(hour -> {
for (EggType type : EggType.entries) {
if (hour == type.resetHour) type.collected = false;
}
});
ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register(literal(SkyblockerMod.NAMESPACE)
.then(literal("eggFinder")
.then(literal("shareLocation")
Expand All @@ -73,7 +96,6 @@ private static void handleLocationChange(Location location) {
armorStandQueue.clear();
return;
}

while (!armorStandQueue.isEmpty()) {
handleArmorStand(armorStandQueue.poll());
}
Expand All @@ -87,7 +109,7 @@ public static void checkIfEgg(ArmorStandEntity armorStand) {
if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableEggFinder) return;
if (SkyblockTime.skyblockSeason.get() != SkyblockTime.Season.SPRING) return;
if (armorStand.hasCustomName() || !armorStand.isInvisible() || !armorStand.shouldHideBasePlate()) return;
if (Utils.getLocation() == Location.UNKNOWN) { //The location is unknown upon world change and will be changed via /locraw soon, so we can queue it for now
if (Utils.getLocation() == Location.UNKNOWN) { //The location is unknown upon world change and will be changed via location change packets soon, so we can queue it for now
armorStandQueue.add(armorStand);
return;
}
Expand All @@ -99,42 +121,19 @@ private static void handleArmorStand(ArmorStandEntity armorStand) {
ItemUtils.getHeadTextureOptional(itemStack).ifPresent(texture -> {
for (EggType type : EggType.entries) { //Compare blockPos rather than entity to avoid incorrect matches when the entity just moves rather than a new one being spawned elsewhere
if (texture.equals(type.texture) && (type.egg == null || !type.egg.entity.getBlockPos().equals(armorStand.getBlockPos()))) {
handleFoundEgg(armorStand, type);
type.egg = new Egg(armorStand, new Waypoint(armorStand.getBlockPos().up(2), SkyblockerConfigManager.get().helpers.chocolateFactory.waypointType, ColorUtils.getFloatComponents(type.color)), false);
return;
}
}
});
}
}

private static void invalidateState() {
if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableEggFinder) return;
isLocationCorrect = false;
for (EggType type : EggType.entries) {
type.egg = null;
}
}

private static void handleFoundEgg(ArmorStandEntity entity, EggType eggType) {
eggType.egg = new Egg(entity, new Waypoint(entity.getBlockPos().up(2), SkyblockerConfigManager.get().helpers.chocolateFactory.waypointType, ColorUtils.getFloatComponents(eggType.color)));

if (!SkyblockerConfigManager.get().helpers.chocolateFactory.sendEggFoundMessages || System.currentTimeMillis() - eggType.messageLastSent < 1000) return;
eggType.messageLastSent = System.currentTimeMillis();
MinecraftClient.getInstance().player.sendMessage(
Constants.PREFIX.get()
.append("Found a ")
.append(Text.literal("Chocolate " + eggType + " Egg")
.withColor(eggType.color))
.append(" at " + entity.getBlockPos().up(2).toShortString() + "!")
.styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker eggFinder shareLocation " + entity.getBlockX() + " " + (entity.getBlockY() + 2) + " " + entity.getBlockZ() + " " + eggType))
.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal("Click to share the location in chat!").formatted(Formatting.GREEN)))));
}

private static void renderWaypoints(WorldRenderContext context) {
if (!SkyblockerConfigManager.get().helpers.chocolateFactory.enableEggFinder) return;
for (EggType type : EggType.entries) {
Egg egg = type.egg;
if (egg != null && egg.waypoint.shouldRender()) egg.waypoint.render(context);
if (egg != null && egg.waypoint.shouldRender() && egg.seen) egg.waypoint.render(context);
}
}

Expand All @@ -143,7 +142,9 @@ private static void onChatMessage(Text text, boolean overlay) {
Matcher matcher = eggFoundPattern.matcher(text.getString());
if (matcher.find()) {
try {
Egg egg = EggType.valueOf(matcher.group(1).toUpperCase()).egg;
EggType eggType = EggType.valueOf(matcher.group(1).toUpperCase());
eggType.collected = true;
Egg egg = eggType.egg;
if (egg != null) egg.waypoint.setFound();
} catch (IllegalArgumentException e) {
logger.error("[Skyblocker Egg Finder] Failed to find egg type for egg found message. Tried to match against: " + matcher.group(0), e);
Expand All @@ -153,43 +154,75 @@ private static void onChatMessage(Text text, boolean overlay) {
matcher.usePattern(newEggPattern);
if (matcher.find()) {
try {
EggType.valueOf(matcher.group(1).toUpperCase()).egg = null;
EggType.valueOf(matcher.group(1).toUpperCase());
} catch (IllegalArgumentException e) {
logger.error("[Skyblocker Egg Finder] Failed to find egg type for egg spawn message. Tried to match against: " + matcher.group(0), e);
}
}
}

record Egg(ArmorStandEntity entity, Waypoint waypoint) {}
static class Egg {
private final ArmorStandEntity entity;
private final Waypoint waypoint;
private boolean seen;

Egg(ArmorStandEntity entity, Waypoint waypoint, boolean seen) {
this.entity = entity;
this.waypoint = waypoint;
this.seen = seen;
}
}

@SuppressWarnings("DataFlowIssue") //Removes that pesky "unboxing of Integer might cause NPE" warning when we already know it's not null
public enum EggType {
LUNCH(Formatting.BLUE.getColorValue(), "ewogICJ0aW1lc3RhbXAiIDogMTcxMTQ2MjU2ODExMiwKICAicHJvZmlsZUlkIiA6ICI3NzUwYzFhNTM5M2Q0ZWQ0Yjc2NmQ4ZGUwOWY4MjU0NiIsCiAgInByb2ZpbGVOYW1lIiA6ICJSZWVkcmVsIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzdhZTZkMmQzMWQ4MTY3YmNhZjk1MjkzYjY4YTRhY2Q4NzJkNjZlNzUxZGI1YTM0ZjJjYmM2NzY2YTAzNTZkMGEiCiAgICB9CiAgfQp9"),
DINNER(Formatting.GREEN.getColorValue(), "ewogICJ0aW1lc3RhbXAiIDogMTcxMTQ2MjY0OTcwMSwKICAicHJvZmlsZUlkIiA6ICI3NGEwMzQxNWY1OTI0ZTA4YjMyMGM2MmU1NGE3ZjJhYiIsCiAgInByb2ZpbGVOYW1lIiA6ICJNZXp6aXIiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZTVlMzYxNjU4MTlmZDI4NTBmOTg1NTJlZGNkNzYzZmY5ODYzMTMxMTkyODNjMTI2YWNlMGM0Y2M0OTVlNzZhOCIKICAgIH0KICB9Cn0"),
BREAKFAST(Formatting.GOLD.getColorValue(), "ewogICJ0aW1lc3RhbXAiIDogMTcxMTQ2MjY3MzE0OSwKICAicHJvZmlsZUlkIiA6ICJiN2I4ZTlhZjEwZGE0NjFmOTY2YTQxM2RmOWJiM2U4OCIsCiAgInByb2ZpbGVOYW1lIiA6ICJBbmFiYW5hbmFZZzciLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTQ5MzMzZDg1YjhhMzE1ZDAzMzZlYjJkZjM3ZDhhNzE0Y2EyNGM1MWI4YzYwNzRmMWI1YjkyN2RlYjUxNmMyNCIKICAgIH0KICB9Cn0");
LUNCH(Formatting.BLUE.getColorValue(), 14, "ewogICJ0aW1lc3RhbXAiIDogMTcxMTQ2MjU2ODExMiwKICAicHJvZmlsZUlkIiA6ICI3NzUwYzFhNTM5M2Q0ZWQ0Yjc2NmQ4ZGUwOWY4MjU0NiIsCiAgInByb2ZpbGVOYW1lIiA6ICJSZWVkcmVsIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzdhZTZkMmQzMWQ4MTY3YmNhZjk1MjkzYjY4YTRhY2Q4NzJkNjZlNzUxZGI1YTM0ZjJjYmM2NzY2YTAzNTZkMGEiCiAgICB9CiAgfQp9"),
DINNER(Formatting.GREEN.getColorValue(), 21, "ewogICJ0aW1lc3RhbXAiIDogMTcxMTQ2MjY0OTcwMSwKICAicHJvZmlsZUlkIiA6ICI3NGEwMzQxNWY1OTI0ZTA4YjMyMGM2MmU1NGE3ZjJhYiIsCiAgInByb2ZpbGVOYW1lIiA6ICJNZXp6aXIiLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZTVlMzYxNjU4MTlmZDI4NTBmOTg1NTJlZGNkNzYzZmY5ODYzMTMxMTkyODNjMTI2YWNlMGM0Y2M0OTVlNzZhOCIKICAgIH0KICB9Cn0"),
BREAKFAST(Formatting.GOLD.getColorValue(), 7, "ewogICJ0aW1lc3RhbXAiIDogMTcxMTQ2MjY3MzE0OSwKICAicHJvZmlsZUlkIiA6ICJiN2I4ZTlhZjEwZGE0NjFmOTY2YTQxM2RmOWJiM2U4OCIsCiAgInByb2ZpbGVOYW1lIiA6ICJBbmFiYW5hbmFZZzciLAogICJzaWduYXR1cmVSZXF1aXJlZCIgOiB0cnVlLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTQ5MzMzZDg1YjhhMzE1ZDAzMzZlYjJkZjM3ZDhhNzE0Y2EyNGM1MWI4YzYwNzRmMWI1YjkyN2RlYjUxNmMyNCIKICAgIH0KICB9Cn0");

private Egg egg = null;
public final int color;
public final String texture;
public final int resetHour;
boolean collected = false;
/*
When a new egg spawns in the player's range, the order of packets/messages goes like this:
set_equipment -> new egg message -> set_entity_data
set_equipment new egg message set_entity_data
We have to set the egg to null to prevent the highlight from staying where it was before the new egg spawned,
and doing so causes the found message to get sent twice. This is the reason for the existence of this field, so that we can not send the 2nd message.
This doesn't fix the field being set twice, but that's not an issue anyway. It'd be much harder to fix the highlight issue mentioned above if it wasn't being set twice.
and doing so causes the found message to get sent twice.
This is the reason for the existence of this field, so that we don't send the 2nd message.
This doesn't fix the field being set twice, but that's not an issue anyway.
It'd be much harder to fix the highlight issue mentioned above if it wasn't being set twice.
*/
private long messageLastSent = 0;

//This is to not create an array each time we iterate over the values
public static final ObjectImmutableList<EggType> entries = ObjectImmutableList.of(EggType.values());

EggType(int color, String texture) {
EggType(int color, int resetHour, String texture) {
this.color = color;
this.resetHour = resetHour;
this.texture = texture;
}

public void setSeen() {
egg.seen = true;
if (!SkyblockerConfigManager.get().helpers.chocolateFactory.sendEggFoundMessages || System.currentTimeMillis() - messageLastSent < 1000) return;
if (collected) {
egg.waypoint.setFound();
return;
}
messageLastSent = System.currentTimeMillis();
MinecraftClient.getInstance().player.sendMessage(
Constants.PREFIX.get()
.append("Found a ")
.append(Text.literal("Chocolate " + this + " Egg")
.withColor(color))
.append(" at " + egg.entity.getBlockPos().up(2).toShortString() + "!")
.styled(style -> style.withClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/skyblocker eggFinder shareLocation " + PosUtils.toSpaceSeparatedString(egg.waypoint.pos) + " " + this))
.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal("Click to share the location in chat!").formatted(Formatting.GREEN)))));
}

@Override
@SuppressWarnings("deprecation") // It's either a new dependency or a deprecated method, and I'd rather use the deprecated method
public String toString() {
return WordUtils.capitalizeFully(this.name());
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/de/hysky/skyblocker/utils/PosUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ public static BlockPos parsePosJson(JsonObject posJson) {
public static String getPosString(BlockPos blockPos) {
return blockPos.getX() + "," + blockPos.getY() + "," + blockPos.getZ();
}

public static String toSpaceSeparatedString(BlockPos blockPos) {
return blockPos.getX() + " " + blockPos.getY() + " " + blockPos.getZ();
}
}
Loading