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

feat: Add a custom ability tree #2390

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
import com.wynntils.features.ui.BulkBuyFeature;
import com.wynntils.features.ui.ContainerScrollFeature;
import com.wynntils.features.ui.CosmeticsPreviewFeature;
import com.wynntils.features.ui.CustomAbilityTreeFeature;
import com.wynntils.features.ui.CustomCharacterSelectionScreenFeature;
import com.wynntils.features.ui.CustomLoadingScreenFeature;
import com.wynntils.features.ui.CustomSeaskipperScreenFeature;
Expand Down Expand Up @@ -329,6 +330,7 @@ public void init() {
registerFeature(new BulkBuyFeature());
registerFeature(new ContainerScrollFeature());
registerFeature(new CosmeticsPreviewFeature());
registerFeature(new CustomAbilityTreeFeature());
registerFeature(new CustomCharacterSelectionScreenFeature());
registerFeature(new CustomLoadingScreenFeature());
registerFeature(new CustomSeaskipperScreenFeature());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright © Wynntils 2023.
* Copyright © Wynntils 2023-2024.
* This file is released under LGPLv3. See LICENSE for full license details.
*/
package com.wynntils.features.debug;
Expand All @@ -9,52 +9,38 @@
import com.wynntils.core.components.Managers;
import com.wynntils.core.components.Models;
import com.wynntils.core.consumers.features.Feature;
import com.wynntils.core.consumers.features.properties.RegisterKeyBind;
import com.wynntils.core.consumers.features.properties.StartDisabled;
import com.wynntils.core.keybinds.KeyBind;
import com.wynntils.core.persisted.config.Category;
import com.wynntils.core.persisted.config.ConfigCategory;
import com.wynntils.mc.event.ContainerClickEvent;
import com.wynntils.models.abilitytree.type.AbilityTreeInfo;
import com.wynntils.models.items.items.gui.AbilityTreeItem;
import com.wynntils.utils.mc.KeyboardUtils;
import com.wynntils.utils.mc.McUtils;
import java.io.File;
import java.util.Locale;
import java.util.Optional;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraftforge.eventbus.api.EventPriority;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import org.lwjgl.glfw.GLFW;

// NOTE: This feature was intented to be used on fully reset ability trees.
// Although support for parsing any tree is present, I would still recommend using a fresh tree to avoid any
// issues.
// Although support for parsing any tree is present,
// a fresh tree must be used for the best results.
@StartDisabled
@ConfigCategory(Category.DEBUG)
public class AbilityTreeDataDumpFeature extends Feature {
private static final File SAVE_FOLDER = WynntilsMod.getModStorageDir("debug");

@SubscribeEvent(priority = EventPriority.HIGHEST)
public void onInventoryClick(ContainerClickEvent event) {
if (!(McUtils.mc().screen instanceof AbstractContainerScreen<?> abstractContainerScreen)) return;
if (!KeyboardUtils.isShiftDown()) return;

Optional<AbilityTreeItem> abilityTreeItem = Models.Item.asWynnItem(event.getItemStack(), AbilityTreeItem.class);

if (abilityTreeItem.isEmpty()) return;

event.setCanceled(true);
McUtils.player().closeContainer();

// Wait for the container to close
Managers.TickScheduler.scheduleNextTick(
() -> Models.AbilityTree.ABILITY_TREE_CONTAINER_QUERIES.dumpAbilityTree(this::saveToDisk));
}
@RegisterKeyBind
public KeyBind dumpAbilityTree = new KeyBind(
"Dump Ability Tree",
GLFW.GLFW_KEY_0,
true,
() -> Models.AbilityTree.ABILITY_TREE_CONTAINER_QUERIES.dumpAbilityTree(this::saveToDisk));

private void saveToDisk(AbilityTreeInfo abilityTreeInfo) {
// Save the dump to a file
JsonElement element = Managers.Json.GSON.toJsonTree(abilityTreeInfo);

String fileName = Models.Character.getClassType().getName().toLowerCase(Locale.ROOT) + "_ablities.json";
String fileName = Models.Character.getClassType().getName().toLowerCase(Locale.ROOT) + "_abilities.json";
File jsonFile = new File(SAVE_FOLDER, fileName);
Managers.Json.savePreciousJson(jsonFile, element.getAsJsonObject());

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright © Wynntils 2023-2024.
* This file is released under LGPLv3. See LICENSE for full license details.
*/
package com.wynntils.features.ui;

import com.wynntils.core.components.Models;
import com.wynntils.core.consumers.features.Feature;
import com.wynntils.core.persisted.config.Category;
import com.wynntils.core.persisted.config.ConfigCategory;
import com.wynntils.mc.event.ContainerClickEvent;
import com.wynntils.models.items.items.gui.AbilityTreeItem;
import com.wynntils.utils.mc.KeyboardUtils;
import com.wynntils.utils.mc.McUtils;
import java.util.Optional;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraftforge.eventbus.api.SubscribeEvent;

@ConfigCategory(Category.UI)
public class CustomAbilityTreeFeature extends Feature {
@SubscribeEvent
public void onInventoryClick(ContainerClickEvent event) {
if (!(McUtils.mc().screen instanceof AbstractContainerScreen<?>)) return;
if (!KeyboardUtils.isShiftDown()) return;

Optional<AbilityTreeItem> abilityTreeItem = Models.Item.asWynnItem(event.getItemStack(), AbilityTreeItem.class);
if (abilityTreeItem.isEmpty()) return;

event.setCanceled(true);

Models.AbilityTree.openCustomAbilityTreeScreen();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
import com.wynntils.handlers.container.type.ContainerContent;
import com.wynntils.models.abilitytree.parser.UnprocessedAbilityTreeInfo;
import com.wynntils.models.abilitytree.type.AbilityTreeInfo;
import com.wynntils.models.abilitytree.type.AbilityTreeInstance;
import com.wynntils.models.abilitytree.type.AbilityTreeNodeState;
import com.wynntils.models.abilitytree.type.AbilityTreeSkillNode;
import com.wynntils.models.abilitytree.type.ParsedAbilityTree;
import com.wynntils.models.containers.ContainerModel;
import com.wynntils.utils.mc.McUtils;
import com.wynntils.utils.type.Pair;
Expand All @@ -40,19 +40,20 @@ public void dumpAbilityTree(Consumer<AbilityTreeInfo> supplier) {
queryAbilityTree(new AbilityTreeContainerQueries.AbilityPageDumper(supplier));
}

public void updateParsedAbilityTree() {
public void parseAbilityTree(AbilityTreeInfo abilityTreeInfo) {
McUtils.player().closeContainer();

// Wait for the container to close
Managers.TickScheduler.scheduleNextTick(() -> queryAbilityTree(
new AbilityTreeContainerQueries.AbilityPageSoftProcessor(Models.AbilityTree::setCurrentAbilityTree)));
Managers.TickScheduler.scheduleNextTick(
() -> queryAbilityTree(new AbilityTreeContainerQueries.AbilityPageSoftProcessor((abilityTreeInstance) ->
Models.AbilityTree.setAbilityTreeInstance(abilityTreeInfo, abilityTreeInstance))));
}

private void queryAbilityTree(AbilityTreeProcessor processor) {
ScriptedContainerQuery query = ScriptedContainerQuery.builder("Ability Tree Query")
.onError(msg -> {
WynntilsMod.warn("Problem querying Ability Tree: " + msg);
McUtils.sendErrorToClient("Dumping Ability Tree failed");
Models.AbilityTree.setAbilityTreeInstance(null, null);
})

// Open character/compass menu
Expand Down Expand Up @@ -139,9 +140,9 @@ protected void processPage(ContainerContent content, int page) {
*/
private static class AbilityPageSoftProcessor extends AbilityTreeProcessor {
private final Map<AbilityTreeSkillNode, AbilityTreeNodeState> collectedInfo = new LinkedHashMap<>();
private final Consumer<ParsedAbilityTree> callback;
private final Consumer<AbilityTreeInstance> callback;

protected AbilityPageSoftProcessor(Consumer<ParsedAbilityTree> callback) {
protected AbilityPageSoftProcessor(Consumer<AbilityTreeInstance> callback) {
this.callback = callback;
}

Expand All @@ -163,7 +164,7 @@ protected void processPage(ContainerContent content, int page) {
boolean lastPage = page == Models.AbilityTree.ABILITY_TREE_PAGES;

if (lastPage) {
callback.accept(new ParsedAbilityTree(ImmutableMap.copyOf(collectedInfo)));
callback.accept(new AbilityTreeInstance(ImmutableMap.copyOf(collectedInfo)));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,44 @@
/*
* Copyright © Wynntils 2023.
* Copyright © Wynntils 2023-2024.
* This file is released under LGPLv3. See LICENSE for full license details.
*/
package com.wynntils.models.abilitytree;

import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.wynntils.core.WynntilsMod;
import com.wynntils.core.components.Managers;
import com.wynntils.core.components.Model;
import com.wynntils.core.components.Models;
import com.wynntils.core.net.Download;
import com.wynntils.core.net.UrlId;
import com.wynntils.mc.event.ScreenClosedEvent;
import com.wynntils.models.abilitytree.parser.AbilityTreeParser;
import com.wynntils.models.abilitytree.type.AbilityTreeInfo;
import com.wynntils.models.abilitytree.type.AbilityTreeNodeState;
import com.wynntils.models.abilitytree.type.AbilityTreeInstance;
import com.wynntils.models.abilitytree.type.AbilityTreeQueryState;
import com.wynntils.models.abilitytree.type.AbilityTreeSkillNode;
import com.wynntils.models.abilitytree.type.ParsedAbilityTree;
import com.wynntils.models.character.type.ClassType;
import com.wynntils.screens.abilities.CustomAbilityTreeScreen;
import com.wynntils.utils.mc.McUtils;
import java.lang.reflect.Type;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import net.minecraftforge.eventbus.api.SubscribeEvent;

public class AbilityTreeModel extends Model {
public static final int ABILITY_TREE_PAGES = 7;
public static final AbilityTreeParser ABILITY_TREE_PARSER = new AbilityTreeParser();
public static final AbilityTreeContainerQueries ABILITY_TREE_CONTAINER_QUERIES = new AbilityTreeContainerQueries();

private Map<ClassType, AbilityTreeInfo> abiliiyTreeMap = new HashMap<>();
private ParsedAbilityTree currentAbilityTree;
// Ability Tree Infos sourced from our data (remote)
private Map<ClassType, AbilityTreeInfo> abilityTreeMap = new EnumMap<>(ClassType.class);

// Parsed Ability Tree Instance and State
private AbilityTreeInstance abilityTreeInstance = null;
private AbilityTreeQueryState abilityTreeQueryState = null;

public AbilityTreeModel() {
super(List.of());
Expand All @@ -41,35 +51,106 @@ public void reloadData() {
Download dl = Managers.Net.download(UrlId.DATA_STATIC_ABILITIES);
dl.handleReader(reader -> {
Type type = new TypeToken<HashMap<String, AbilityTreeInfo>>() {}.getType();
Gson gson = new GsonBuilder().create();

Map<String, AbilityTreeInfo> abilityMap = gson.fromJson(reader, type);
Map<String, AbilityTreeInfo> abilityMap = Managers.Json.GSON.fromJson(reader, type);

Map<ClassType, AbilityTreeInfo> tempMap = new HashMap<>();
Map<ClassType, AbilityTreeInfo> tempMap = new EnumMap<>(ClassType.class);

abilityMap.forEach((key, value) -> tempMap.put(ClassType.fromName(key), value));

abiliiyTreeMap = tempMap;
abilityTreeMap = tempMap;
});
}

public void setCurrentAbilityTree(ParsedAbilityTree currentAbilityTree) {
this.currentAbilityTree = currentAbilityTree;
@SubscribeEvent
public void onScreenClosed(ScreenClosedEvent event) {
if (!(event.getScreen() instanceof CustomAbilityTreeScreen)) return;

abilityTreeInstance = null;
abilityTreeQueryState = null;
}

public AbilityTreeNodeState getNodeState(AbilityTreeSkillNode node) {
if (currentAbilityTree == null) {
return AbilityTreeNodeState.LOCKED;
public void openCustomAbilityTreeScreen() {
if (abilityTreeInstance != null || abilityTreeQueryState != null) {
WynntilsMod.warn("Opened an ability tree screen while one was already open. This should not happen.");
// try continuing anyway
}

abilityTreeInstance = null;
abilityTreeQueryState = null;

// Check if the player has a class parsed
if (Models.Character.getClassType() == ClassType.NONE) {
setAbilityTreeQueryState(AbilityTreeQueryState.ERROR_CLASS_NOT_PARSED);
}

// Check if we have the ability tree info for the class
if (Models.Character.hasCharacter() && !abilityTreeMap.containsKey(Models.Character.getClassType())) {
setAbilityTreeQueryState(AbilityTreeQueryState.ERROR_NO_CLASS_DATA);
}

// Start parsing the ability tree instance, if there are no errors
if (abilityTreeQueryState == null) {
setAbilityTreeQueryState(AbilityTreeQueryState.PARSING);
ABILITY_TREE_CONTAINER_QUERIES.parseAbilityTree(abilityTreeMap.get(Models.Character.getClassType()));
}

// Open the custom ability tree screen
McUtils.mc().setScreen(new CustomAbilityTreeScreen());
}

public void setAbilityTreeInstance(AbilityTreeInfo abilityTreeInfo, AbilityTreeInstance abilityTreeInstance) {
if (abilityTreeInfo == null || abilityTreeInstance == null) {
// We have an error, set the state to error
setAbilityTreeQueryState(AbilityTreeQueryState.ERROR_PARSING_INSTANCE);
return;
}

// Validate that the ability tree instance matches the ability tree info
if (!validateAbilityTreeInstance(abilityTreeInfo, abilityTreeInstance)) {
// We have an error, set the state to error
setAbilityTreeQueryState(AbilityTreeQueryState.ERROR_API_INFO_OUTDATED);
return;
}

return currentAbilityTree.nodes().keySet().stream()
.filter(n -> n.equals(node))
.map(currentAbilityTree.nodes()::get)
.findFirst()
.orElse(AbilityTreeNodeState.LOCKED);
this.abilityTreeInstance = abilityTreeInstance;
setAbilityTreeQueryState(AbilityTreeQueryState.PARSED);
}

public AbilityTreeInfo getAbilityTree(ClassType type) {
return abiliiyTreeMap.get(type);
public Optional<AbilityTreeInfo> getAbilityTreeInfo() {
return Optional.ofNullable(abilityTreeMap.get(Models.Character.getClassType()));
}

public Optional<AbilityTreeInstance> getAbilityTreeInstance() {
return Optional.ofNullable(abilityTreeInstance);
}

public AbilityTreeQueryState getAbilityTreeQueryState() {
return abilityTreeQueryState;
}

private void setAbilityTreeQueryState(AbilityTreeQueryState abilityTreeQueryState) {
this.abilityTreeQueryState = abilityTreeQueryState;

if (McUtils.mc().screen instanceof CustomAbilityTreeScreen abilityTreeScreen) {
abilityTreeScreen.onAbilityTreeQueryStateChanged(abilityTreeQueryState);
}
}

private boolean validateAbilityTreeInstance(
AbilityTreeInfo abilityTreeInfo, AbilityTreeInstance abilityTreeInstance) {
// Firstly, check node counts
if (abilityTreeInfo.nodes().size() != abilityTreeInstance.nodes().size()) {
return false;
}

// Secondly, check if all nodes can be found in the instance
for (AbilityTreeSkillNode node : abilityTreeInfo.nodes()) {
if (!abilityTreeInstance.nodes().containsKey(node)) {
return false;
}
}

return true;
}
}