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
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
|
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 org.apache.commons.math3.geometry.euclidean.threed.Line;
import org.apache.commons.math3.geometry.euclidean.threed.Vector3D;
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 maximum distance a particle can be from the last to be considered part of the line
*/
private static final double PARTICLES_MAX_DISTANCE = 0.9;
/**
* 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 = 8;
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();
private static Vec3d particleLastPos = Vec3d.ZERO;
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();
Vec3d particleLastPos = Vec3d.ZERO;
}
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.translatable("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;
};
}
/**
* Verify that a location could be correct and not to far out of zone. This is a problem when areas sometimes do not exist and is not a perfect solution
* @param startingZone zone player is searching in
* @param pos location where the area should be
* @return corrected location
*/
private static Boolean verifyLocation(ZONE startingZone, Vec3d pos) {
return ZONE_BOUNDING_BOXES.get(startingZone).expand(100, 0, 100).contains(pos);
}
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();
//ignore particle not in the line
if (particlePos.distanceTo(particleLastPos) > PARTICLES_MAX_DISTANCE) {
return;
}
particleLastPos = particlePos;
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
ZONE playerZone = getZoneOfLocation(startPosOne);
MiningLocationLabel.CrystalHollowsLocationsCategory location = getTargetLocation(playerZone);
if (!verifyLocation(playerZone, targetLocation)) {
location = MiningLocationLabel.CrystalHollowsLocationsCategory.UNKNOWN;
}
//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) {
//convert format to get lines for the intersection solving
Vector3D lineOneStart = new Vector3D(startPosOne.x, startPosOne.y, startPosOne.z);
Vector3D lineOneEnd = new Vector3D(directionOne.x, directionOne.y, directionOne.z).add(lineOneStart);
Vector3D lineTwoStart = new Vector3D(startPosTwo.x, startPosTwo.y, startPosTwo.z);
Vector3D lineTwoEnd = new Vector3D(directionTwo.x, directionTwo.y, directionTwo.z).add(lineTwoStart);
Line line = new Line(lineOneStart, lineOneEnd, 1);
Line lineTwo = new Line(lineTwoStart, lineTwoEnd, 1);
Vector3D intersection = line.intersection(lineTwo);
//return final target location
if (intersection == null || intersection.equals(new Vector3D(0, 0, 0))) {
return null;
}
return new Vec3d(intersection.getX(), intersection.getY(), intersection.getZ());
}
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);
reset();
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);
reset();
startNewState(SolverStates.PROCESSING_FIRST_USE);
}
}
}
return false;
}
private static void startNewState(SolverStates newState) {
if (CLIENT.player == null) {
return;
}
//get where eye pos independent of if player is crouching
Vec3d playerPos = CLIENT.player.getPos().add(0, 1.62, 0);
if (newState == SolverStates.PROCESSING_FIRST_USE) {
currentState = SolverStates.PROCESSING_FIRST_USE;
startPosOne = playerPos;
particleLastUpdate = System.currentTimeMillis();
particleLastPos = playerPos;
} else if (newState == SolverStates.PROCESSING_SECOND_USE) {
currentState = SolverStates.PROCESSING_SECOND_USE;
startPosTwo = playerPos;
particleLastUpdate = System.currentTimeMillis();
particleLastPos = playerPos;
}
}
}
|