aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/de/hysky/skyblocker/skyblock/dwarven/WishingCompassSolver.java
blob: 7ba2e05a0a20f396fda134fbc2b0a0e2ae4fdaa4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
package de.hysky.skyblocker.skyblock.dwarven;

import de.hysky.skyblocker.config.SkyblockerConfigManager;
import de.hysky.skyblocker.skyblock.tabhud.util.PlayerListMgr;
import de.hysky.skyblocker.utils.Constants;
import de.hysky.skyblocker.utils.Utils;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents;
import net.fabricmc.fabric.api.event.player.UseBlockCallback;
import net.fabricmc.fabric.api.event.player.UseItemCallback;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.PlayerListEntry;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.network.packet.s2c.play.ParticleS2CPacket;
import net.minecraft.particle.ParticleTypes;
import net.minecraft.text.Text;
import net.minecraft.util.*;
import net.minecraft.util.hit.BlockHitResult;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Box;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;

public class WishingCompassSolver {
    private static final MinecraftClient CLIENT = MinecraftClient.getInstance();

    enum SolverStates {
        NOT_STARTED,
        PROCESSING_FIRST_USE,
        WAITING_FOR_SECOND,
        PROCESSING_SECOND_USE,
    }

    enum ZONE {
        CRYSTAL_NUCLEUS,
        JUNGLE,
        MITHRIL_DEPOSITS,
        GOBLIN_HOLDOUT,
        PRECURSOR_REMNANTS,
        MAGMA_FIELDS,
    }

    private static final HashMap<ZONE, Box> ZONE_BOUNDING_BOXES = Util.make(new HashMap<>(), map -> {
        map.put(ZONE.CRYSTAL_NUCLEUS, new Box(462, 63, 461, 564, 181, 565));
        map.put(ZONE.JUNGLE, new Box(201, 63, 201, 513, 189, 513));
        map.put(ZONE.MITHRIL_DEPOSITS, new Box(512, 63, 201, 824, 189, 513));
        map.put(ZONE.GOBLIN_HOLDOUT, new Box(201, 63, 512, 513, 189, 824));
        map.put(ZONE.PRECURSOR_REMNANTS, new Box(512, 63, 512, 824, 189, 824));
        map.put(ZONE.MAGMA_FIELDS, new Box(201, 30, 201, 824, 64, 824));
    });
    private static final Vec3d JUNGLE_TEMPLE_DOOR_OFFSET = new Vec3d(-57, 36, -21);
    /**
     * how many particles to use to get direction of a line
     */
    private static final long PARTICLES_PER_LINE = 25;
    /**
     * the amount of milliseconds to wait for the next particle until assumed failed
     */
    private static final long PARTICLES_MAX_DELAY = 500;
    /**
     * the distance the player has to be from where they used the first compass to where they use the second
     */
    private static final long DISTANCE_BETWEEN_USES = 32;

    private static SolverStates currentState = SolverStates.NOT_STARTED;
    private static Vec3d startPosOne = Vec3d.ZERO;
    private static Vec3d startPosTwo = Vec3d.ZERO;
    private static Vec3d directionOne = Vec3d.ZERO;
    private static Vec3d directionTwo = Vec3d.ZERO;
    private static long particleUsedCountOne = 0;
    private static long particleUsedCountTwo = 0;
    private static long particleLastUpdate = System.currentTimeMillis();


    public static void init() {
        UseItemCallback.EVENT.register(WishingCompassSolver::onItemInteract);
        UseBlockCallback.EVENT.register(WishingCompassSolver::onBlockInteract);
        ClientPlayConnectionEvents.JOIN.register((_handler, _sender, _client) -> reset());
    }

    private static void reset() {
        currentState = SolverStates.NOT_STARTED;
        startPosOne = Vec3d.ZERO;
        startPosTwo = Vec3d.ZERO;
        directionOne = Vec3d.ZERO;
        directionTwo = Vec3d.ZERO;
        particleUsedCountOne = 0;
        particleUsedCountTwo = 0;
        particleLastUpdate = System.currentTimeMillis();
    }

    private static boolean isKingsScentPresent() {
        String footer = PlayerListMgr.getFooter();
        if (footer == null) {
            return false;
        }
        return footer.contains("King's Scent I");
    }

    private static boolean isKeyInInventory() {
        if (CLIENT.player == null) {
            return false;
        }
        for (ItemStack item : CLIENT.player.getInventory().main) {
            if (item != null && Objects.equals(item.getSkyblockId(), "JUNGLE_KEY")) {
                return true;
            }
        }
        return false;
    }

    private static ZONE getZoneOfLocation(Vec3d location) {
        for (Map.Entry<ZONE, Box> zone : ZONE_BOUNDING_BOXES.entrySet()) {
            if (zone.getValue().contains(location)) {
                return zone.getKey();
            }
        }

        //default to nucleus if somehow not in another zone
        return ZONE.CRYSTAL_NUCLEUS;
    }

    private static Boolean isZoneComplete(ZONE zone) {
        if (CLIENT.getNetworkHandler() == null || CLIENT.player == null) {
            return false;
        }
        //creates cleaned stream of all the entry's in tab list
        Stream<PlayerListEntry> playerListStream = CLIENT.getNetworkHandler().getPlayerList().stream();
        Stream<String> displayNameStream = playerListStream.map(PlayerListEntry::getDisplayName).filter(Objects::nonNull).map(Text::getString).map(String::strip);

        //make sure the data is in tab and if not tell the user
        if (displayNameStream.noneMatch(entry -> entry.equals("Crystals:"))) {
            CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.literal("skyblocker.config.mining.crystalsWaypoints.wishingCompassSolver.enableTabMessage")), false);
            return false;
        }

        //return if the crystal for a zone is found
        playerListStream = CLIENT.getNetworkHandler().getPlayerList().stream();
        displayNameStream = playerListStream.map(PlayerListEntry::getDisplayName).filter(Objects::nonNull).map(Text::getString).map(String::strip);
        return switch (zone) {
            case JUNGLE -> displayNameStream.noneMatch(entry -> entry.equals("Amethyst: ✖ Not Found"));
            case MITHRIL_DEPOSITS -> displayNameStream.noneMatch(entry -> entry.equals("Jade: ✖ Not Found"));
            case GOBLIN_HOLDOUT -> displayNameStream.noneMatch(entry -> entry.equals("Amber: ✖ Not Found"));
            case PRECURSOR_REMNANTS -> displayNameStream.noneMatch(entry -> entry.equals("Sapphire: ✖ Not Found"));
            case MAGMA_FIELDS -> displayNameStream.noneMatch(entry -> entry.equals("Topaz: ✖ Not Found"));
            default -> false;
        };
    }

    private static MiningLocationLabel.CrystalHollowsLocationsCategory getTargetLocation(ZONE startingZone) {
        //if the zone is complete return null
        if (isZoneComplete(startingZone)) {
            return MiningLocationLabel.CrystalHollowsLocationsCategory.UNKNOWN;
        }
        return switch (startingZone) {
            case JUNGLE ->
                    isKeyInInventory() ? MiningLocationLabel.CrystalHollowsLocationsCategory.JUNGLE_TEMPLE : MiningLocationLabel.CrystalHollowsLocationsCategory.ODAWA;
            case MITHRIL_DEPOSITS -> MiningLocationLabel.CrystalHollowsLocationsCategory.MINES_OF_DIVAN;
            case GOBLIN_HOLDOUT ->
                    isKingsScentPresent() ? MiningLocationLabel.CrystalHollowsLocationsCategory.GOBLIN_QUEENS_DEN : MiningLocationLabel.CrystalHollowsLocationsCategory.KING_YOLKAR;
            case PRECURSOR_REMNANTS -> MiningLocationLabel.CrystalHollowsLocationsCategory.LOST_PRECURSOR_CITY;
            case MAGMA_FIELDS -> MiningLocationLabel.CrystalHollowsLocationsCategory.KHAZAD_DUM;
            default -> MiningLocationLabel.CrystalHollowsLocationsCategory.UNKNOWN;
        };
    }


    public static void onParticle(ParticleS2CPacket packet) {
        if (!Utils.isInCrystalHollows() || !ParticleTypes.HAPPY_VILLAGER.equals(packet.getParameters().getType())) {
            return;
        }
        //get location of particle
        Vec3d particlePos = new Vec3d(packet.getX(), packet.getY(), packet.getZ());
        //update particle used time
        particleLastUpdate = System.currentTimeMillis();

        switch (currentState) {
            case PROCESSING_FIRST_USE -> {
                Vec3d particleDirection = particlePos.subtract(startPosOne).normalize();
                //move direction to fit with particle
                directionOne = directionOne.add(particleDirection.multiply((double) 1 / PARTICLES_PER_LINE));
                particleUsedCountOne += 1;
                //if used enough particle go to next state
                if (particleUsedCountOne >= PARTICLES_PER_LINE) {
                    currentState = SolverStates.WAITING_FOR_SECOND;
                    if (CLIENT.player != null) {
                        CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.config.mining.crystalsWaypoints.wishingCompassSolver.wishingCompassUsedMessage").formatted(Formatting.GREEN)), false);
                    }
                }
            }
            case PROCESSING_SECOND_USE -> {
                Vec3d particleDirection = particlePos.subtract(startPosTwo).normalize();
                //move direction to fit with particle
                directionTwo = directionTwo.add(particleDirection.multiply((double) 1 / PARTICLES_PER_LINE));
                particleUsedCountTwo += 1;
                //if used enough particle go to next state
                if (particleUsedCountTwo >= PARTICLES_PER_LINE) {
                    processSolution();
                }
            }
        }
    }

    private static void processSolution() {
        if (CLIENT.player == null) {
            reset();
            return;
        }
        Vec3d targetLocation = solve(startPosOne, startPosTwo, directionOne, directionTwo);
        if (targetLocation == null) {
            CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.config.mining.crystalsWaypoints.wishingCompassSolver.somethingWentWrongMessage").formatted(Formatting.RED)), false);
        } else {
            //send message to player with location and name
            MiningLocationLabel.CrystalHollowsLocationsCategory location = getTargetLocation(getZoneOfLocation(startPosOne));
            //offset the jungle location to its doors
            if (location == MiningLocationLabel.CrystalHollowsLocationsCategory.JUNGLE_TEMPLE) {
                targetLocation = targetLocation.add(JUNGLE_TEMPLE_DOOR_OFFSET);
            }

            CLIENT.player.sendMessage(Constants.PREFIX.get()
                            .append(Text.translatable("skyblocker.config.mining.crystalsWaypoints.wishingCompassSolver.foundMessage").formatted(Formatting.GREEN))
                            .append(Text.literal(location.getName()).withColor(location.getColor()))
                            .append(Text.literal(": " + (int) targetLocation.getX() + " " + (int) targetLocation.getY() + " " + (int) targetLocation.getZ())),
                    false);

            //add waypoint
            CrystalsLocationsManager.addCustomWaypoint(location.getName(), BlockPos.ofFloored(targetLocation));
        }

        //reset ready for another go
        reset();
    }

    /**
     * using the stating locations and line direction solve for where the location must be
     */
    protected static Vec3d solve(Vec3d startPosOne, Vec3d startPosTwo, Vec3d directionOne, Vec3d directionTwo) {
        Vec3d crossProduct = directionOne.crossProduct(directionTwo);
        if (crossProduct.equals(Vec3d.ZERO)) {
            //lines are parallel or coincident
            return null;
        }
        // Calculate the difference vector between startPosTwo and startPosOne
        Vec3d diff = startPosTwo.subtract(startPosOne);
        // projecting 'diff' onto the plane defined by 'directionTwo' and 'crossProduct'. then scaling by the inverse squared length of 'crossProduct'
        double intersectionScalar = diff.dotProduct(directionTwo.crossProduct(crossProduct)) / crossProduct.lengthSquared();
        // if intersectionScalar is a negative number the lines are meeting in the opposite direction and giving incorrect cords
        if (intersectionScalar < 0) {
            return null;
        }
        //get final target location
        return startPosOne.add(directionOne.multiply(intersectionScalar));
    }

    private static ActionResult onBlockInteract(PlayerEntity playerEntity, World world, Hand hand, BlockHitResult blockHitResult) {
        if (CLIENT.player == null) {
            return null;
        }
        ItemStack stack = CLIENT.player.getStackInHand(hand);
        //make sure the user is in the crystal hollows and holding the wishing compass
        if (!Utils.isInCrystalHollows() || !SkyblockerConfigManager.get().mining.crystalsWaypoints.WishingCompassSolver || !Objects.equals(stack.getSkyblockId(), "WISHING_COMPASS")) {
            return ActionResult.PASS;
        }
        if (useCompass()) {
            return ActionResult.FAIL;
        }

        return ActionResult.PASS;
    }

    private static TypedActionResult<ItemStack> onItemInteract(PlayerEntity playerEntity, World world, Hand hand) {
        if (CLIENT.player == null) {
            return null;
        }
        ItemStack stack = CLIENT.player.getStackInHand(hand);
        //make sure the user is in the crystal hollows and holding the wishing compass
        if (!Utils.isInCrystalHollows() || !SkyblockerConfigManager.get().mining.crystalsWaypoints.WishingCompassSolver || !Objects.equals(stack.getSkyblockId(), "WISHING_COMPASS")) {
            return TypedActionResult.pass(stack);
        }
        if (useCompass()) {
            return TypedActionResult.fail(stack);
        }

        return TypedActionResult.pass(stack);
    }

    /**
     * Computes what to do next when a compass is used.
     *
     * @return if the use event should be canceled
     */
    private static boolean useCompass() {
        if (CLIENT.player == null) {
            return true;
        }
        Vec3d playerPos = CLIENT.player.getEyePos();
        ZONE currentZone = getZoneOfLocation(playerPos);

        switch (currentState) {
            case NOT_STARTED -> {
                //do not start if the player is in nucleus as this does not work well
                if (currentZone == ZONE.CRYSTAL_NUCLEUS) {
                    CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.config.mining.crystalsWaypoints.wishingCompassSolver.useOutsideNucleusMessage")), false);
                    return true;
                }
                startNewState(SolverStates.PROCESSING_FIRST_USE);
            }

            case WAITING_FOR_SECOND -> {
                //only continue if the player is far enough away from the first position to get a better reading
                if (startPosOne.distanceTo(playerPos) < DISTANCE_BETWEEN_USES) {
                    CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.config.mining.crystalsWaypoints.wishingCompassSolver.moveFurtherMessage")), false);
                    return true;
                } else {
                    //make sure the player is in the same zone as they used to first or restart
                    if (currentZone != getZoneOfLocation(startPosOne)) {
                        CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.config.mining.crystalsWaypoints.wishingCompassSolver.changingZoneMessage")), false);
                        startNewState(SolverStates.PROCESSING_FIRST_USE);
                    } else {
                        startNewState(SolverStates.PROCESSING_SECOND_USE);
                    }
                }
            }

            case PROCESSING_FIRST_USE, PROCESSING_SECOND_USE -> {
                //if still looking for particles for line tell the user to wait
                //else tell the use something went wrong and its starting again
                if (System.currentTimeMillis() - particleLastUpdate < PARTICLES_MAX_DELAY) {
                    CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.config.mining.crystalsWaypoints.wishingCompassSolver.waitLongerMessage").formatted(Formatting.RED)), false);
                    return true;
                } else {
                    CLIENT.player.sendMessage(Constants.PREFIX.get().append(Text.translatable("skyblocker.config.mining.crystalsWaypoints.wishingCompassSolver.couldNotDetectLastUseMessage").formatted(Formatting.RED)), false);
                    startNewState(SolverStates.PROCESSING_FIRST_USE);
                }
            }
        }

        return false;
    }

    private static void startNewState(SolverStates newState) {
        if (CLIENT.player == null) {
            return;
        }
        Vec3d playerPos = CLIENT.player.getEyePos();

        if (newState == SolverStates.PROCESSING_FIRST_USE) {
            currentState = SolverStates.PROCESSING_FIRST_USE;
            startPosOne = playerPos;
            particleLastUpdate = System.currentTimeMillis();
        } else if (newState == SolverStates.PROCESSING_SECOND_USE) {
            currentState = SolverStates.PROCESSING_SECOND_USE;
            startPosTwo = playerPos;
            particleLastUpdate = System.currentTimeMillis();
        }
    }
}