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
|
package de.hysky.skyblocker.skyblock.item.tooltip;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.logging.LogUtils;
import com.mojang.serialization.Codec;
import com.mojang.serialization.JsonOps;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import com.mojang.util.UndashedUuid;
import de.hysky.skyblocker.SkyblockerMod;
import de.hysky.skyblocker.utils.ItemUtils;
import de.hysky.skyblocker.utils.Utils;
import it.unimi.dsi.fastutil.Pair;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents;
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.ingame.GenericContainerScreen;
import net.minecraft.screen.GenericContainerScreenHandler;
import net.minecraft.screen.slot.Slot;
import org.slf4j.Logger;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import java.util.function.ToIntFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class AccessoriesHelper {
private static final Logger LOGGER = LogUtils.getLogger();
private static final Path FILE = SkyblockerMod.CONFIG_DIR.resolve("collected_accessories.json");
private static final Pattern ACCESSORY_BAG_TITLE = Pattern.compile("Accessory Bag(?: \\((?<page>\\d+)\\/\\d+\\))?");
//UUID -> Profile Id & Data
private static final Object2ObjectOpenHashMap<String, Object2ObjectOpenHashMap<String, ProfileAccessoryData>> COLLECTED_ACCESSORIES = new Object2ObjectOpenHashMap<>();
private static final Predicate<String> NON_EMPTY = s -> !s.isEmpty();
private static final Predicate<Accessory> HAS_FAMILY = Accessory::hasFamily;
private static final ToIntFunction<Accessory> ACCESSORY_TIER = Accessory::tier;
private static Map<String, Accessory> ACCESSORY_DATA = new Object2ObjectOpenHashMap<>();
//remove??
private static CompletableFuture<Void> loaded;
public static void init() {
ClientLifecycleEvents.CLIENT_STARTED.register((_client) -> load());
ClientLifecycleEvents.CLIENT_STOPPING.register((_client) -> save());
ScreenEvents.BEFORE_INIT.register((_client, screen, _scaledWidth, _scaledHeight) -> {
if (Utils.isOnSkyblock() && TooltipInfoType.ACCESSORIES.isTooltipEnabled() && !Utils.getProfileId().isEmpty() && screen instanceof GenericContainerScreen genericContainerScreen) {
Matcher matcher = ACCESSORY_BAG_TITLE.matcher(genericContainerScreen.getTitle().getString());
if (matcher.matches()) {
ScreenEvents.afterTick(screen).register(_screen -> {
GenericContainerScreenHandler handler = genericContainerScreen.getScreenHandler();
int page = matcher.group("page") != null ? Integer.parseInt(matcher.group("page")) : 1;
collectAccessories(handler.slots.subList(0, handler.getRows() * 9), page);
});
}
}
});
}
//Note: JsonOps.COMPRESSED must be used if you're using maps with non-string keys
private static void load() {
loaded = CompletableFuture.runAsync(() -> {
try (BufferedReader reader = Files.newBufferedReader(FILE)) {
COLLECTED_ACCESSORIES.putAll(ProfileAccessoryData.SERIALIZATION_CODEC.parse(JsonOps.COMPRESSED, JsonParser.parseReader(reader)).getOrThrow());
} catch (NoSuchFileException ignored) {
} catch (Exception e) {
LOGGER.error("[Skyblocker Accessory Helper] Failed to load accessory file!", e);
}
});
}
private static void save() {
try (BufferedWriter writer = Files.newBufferedWriter(FILE)) {
SkyblockerMod.GSON.toJson(ProfileAccessoryData.SERIALIZATION_CODEC.encodeStart(JsonOps.COMPRESSED, COLLECTED_ACCESSORIES).getOrThrow(), writer);
} catch (Exception e) {
LOGGER.error("[Skyblocker Accessory Helper] Failed to save accessory file!", e);
}
}
private static void collectAccessories(List<Slot> slots, int page) {
//Is this even needed?
if (!loaded.isDone()) return;
List<String> accessoryIds = slots.stream()
.map(Slot::getStack)
.map(ItemUtils::getItemId)
.filter(NON_EMPTY)
.toList();
String uuid = UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull());
COLLECTED_ACCESSORIES.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>()).computeIfAbsent(Utils.getProfileId(), profileId -> ProfileAccessoryData.createDefault()).pages()
.put(page, new ObjectOpenHashSet<>(accessoryIds));
}
public static Pair<AccessoryReport, String> calculateReport4Accessory(String accessoryId) {
if (!ACCESSORY_DATA.containsKey(accessoryId) || Utils.getProfileId().isEmpty()) return Pair.of(AccessoryReport.INELIGIBLE, null);
Accessory accessory = ACCESSORY_DATA.get(accessoryId);
String uuid = UndashedUuid.toString(MinecraftClient.getInstance().getSession().getUuidOrNull());
Set<Accessory> collectedAccessories = COLLECTED_ACCESSORIES.computeIfAbsent(uuid, _uuid -> new Object2ObjectOpenHashMap<>()).computeIfAbsent(Utils.getProfileId(), profileId -> ProfileAccessoryData.createDefault()).pages().values().stream()
.flatMap(ObjectOpenHashSet::stream)
.filter(ACCESSORY_DATA::containsKey)
.map(ACCESSORY_DATA::get)
.collect(Collectors.toSet());
// If the accessory doesn't belong to a family
if (accessory.family().isEmpty()) {
//If the player has this accessory or player doesn't have this accessory
return collectedAccessories.contains(accessory) ? Pair.of(AccessoryReport.HAS_HIGHEST_TIER, null) : Pair.of(AccessoryReport.MISSING, "");
}
Predicate<Accessory> HAS_SAME_FAMILY = accessory::hasSameFamily;
Set<Accessory> collectedAccessoriesInTheSameFamily = collectedAccessories.stream()
.filter(HAS_FAMILY)
.filter(HAS_SAME_FAMILY)
.collect(Collectors.toSet());
Set<Accessory> accessoriesInTheSameFamily = ACCESSORY_DATA.values().stream()
.filter(HAS_FAMILY)
.filter(HAS_SAME_FAMILY)
.collect(Collectors.toSet());
int highestTierInFamily = accessoriesInTheSameFamily.stream()
.mapToInt(ACCESSORY_TIER)
.max()
.orElse(0);
//If the player hasn't collected any accessory in same family
if (collectedAccessoriesInTheSameFamily.isEmpty()) return Pair.of(AccessoryReport.MISSING, String.format("(%d/%d)", accessory.tier(), highestTierInFamily));
int highestTierCollectedInFamily = collectedAccessoriesInTheSameFamily.stream()
.mapToInt(ACCESSORY_TIER)
.max()
.getAsInt();
//If this accessory is the highest tier, and the player has the highest tier accessory in this family
//This accounts for multiple accessories with the highest tier
if (accessory.tier() == highestTierInFamily && highestTierCollectedInFamily == highestTierInFamily) return Pair.of(AccessoryReport.HAS_HIGHEST_TIER, null);
//If this accessory is a higher tier than all the other collected accessories in the same family
if (accessory.tier() > highestTierCollectedInFamily) return Pair.of(AccessoryReport.IS_GREATER_TIER, String.format("(%d→%d/%d)", highestTierCollectedInFamily, accessory.tier(), highestTierInFamily));
//If this accessory is a lower tier than one already obtained from same family
if (accessory.tier() < highestTierCollectedInFamily) return Pair.of(AccessoryReport.OWNS_BETTER_TIER, String.format("(%d→%d/%d)", highestTierCollectedInFamily, accessory.tier(), highestTierInFamily));
//If there is an accessory in the same family that has a higher tier
if (accessory.tier() < highestTierInFamily) return Pair.of(AccessoryReport.HAS_GREATER_TIER, String.format("(%d/%d)", highestTierCollectedInFamily, highestTierInFamily));
return Pair.of(AccessoryReport.MISSING, String.format("(%d/%d)", accessory.tier(), highestTierInFamily));
}
static void refreshData(JsonObject data) {
try {
ACCESSORY_DATA = Accessory.MAP_CODEC.parse(JsonOps.INSTANCE, data).getOrThrow();
} catch (Exception e) {
LOGGER.error("[Skyblocker Accessory Helper] Failed to parse data!", e);
}
}
private record ProfileAccessoryData(Int2ObjectOpenHashMap<ObjectOpenHashSet<String>> pages) {
private static final Codec<ProfileAccessoryData> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.unboundedMap(Codec.INT, Codec.STRING.listOf().xmap(ObjectOpenHashSet::new, ObjectArrayList::new))
.xmap(Int2ObjectOpenHashMap::new, Int2ObjectOpenHashMap::new).fieldOf("pages").forGetter(ProfileAccessoryData::pages)
).apply(instance, ProfileAccessoryData::new));
private static final Codec<Object2ObjectOpenHashMap<String, Object2ObjectOpenHashMap<String, ProfileAccessoryData>>> SERIALIZATION_CODEC = Codec.unboundedMap(Codec.STRING, Codec.unboundedMap(Codec.STRING, CODEC)
.xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new)
).xmap(Object2ObjectOpenHashMap::new, Object2ObjectOpenHashMap::new);
private static ProfileAccessoryData createDefault() {
return new ProfileAccessoryData(new Int2ObjectOpenHashMap<>());
}
}
/**
* @author AzureAaron
* @implSpec <a href="https://github.com/AzureAaron/aaron-mod/blob/1.20/src/main/java/net/azureaaron/mod/commands/MagicalPowerCommand.java#L475">Aaron's Mod</a>
*/
private record Accessory(String id, Optional<String> family, int tier) {
private static final Codec<Accessory> CODEC = RecordCodecBuilder.create(instance -> instance.group(
Codec.STRING.fieldOf("id").forGetter(Accessory::id),
Codec.STRING.optionalFieldOf("family").forGetter(Accessory::family),
Codec.INT.optionalFieldOf("tier", 0).forGetter(Accessory::tier)
).apply(instance, Accessory::new));
private static final Codec<Map<String, Accessory>> MAP_CODEC = Codec.unboundedMap(Codec.STRING, CODEC);
private boolean hasFamily() {
return family.isPresent();
}
private boolean hasSameFamily(Accessory other) {
return other.family().equals(this.family);
}
}
public enum AccessoryReport {
HAS_HIGHEST_TIER, //You've collected the highest tier - Collected
IS_GREATER_TIER, //This accessory is an upgrade from the one in the same family that you already have - Upgrade -- Shows you what tier this accessory is in its family
HAS_GREATER_TIER, //This accessory has a higher tier upgrade - Upgradable -- Shows you the highest tier accessory you've collected in that family
OWNS_BETTER_TIER, //You've collected an accessory in this family with a higher tier - Downgrade -- Shows you the highest tier accessory you've collected in that family
MISSING, //You don't have any accessories in this family - Missing
INELIGIBLE
}
}
|