aboutsummaryrefslogtreecommitdiff
path: root/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'src/main')
-rw-r--r--src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java36
-rw-r--r--src/main/java/moe/nea/firmament/init/MixinPlugin.java103
-rw-r--r--src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java183
-rw-r--r--src/main/java/moe/nea/firmament/mixins/CopyChatPatch.java44
-rw-r--r--src/main/java/moe/nea/firmament/mixins/DispatchMouseInputEventsPatch.java17
-rw-r--r--src/main/java/moe/nea/firmament/mixins/HudRenderEventsPatch.java7
-rw-r--r--src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java13
-rw-r--r--src/main/java/moe/nea/firmament/mixins/MinecraftInitLevelListener.java26
-rw-r--r--src/main/java/moe/nea/firmament/mixins/MixinRecipeBookScreen.java16
-rw-r--r--src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java2
-rw-r--r--src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java8
-rw-r--r--src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java30
-rw-r--r--src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java6
-rw-r--r--src/main/java/moe/nea/firmament/mixins/accessor/AccessorChatHud.java10
-rw-r--r--src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java2
-rw-r--r--src/main/java/moe/nea/firmament/mixins/accessor/AccessorPlayerListHud.java31
-rw-r--r--src/main/java/moe/nea/firmament/mixins/feature/DisableSlotHighlights.java25
-rw-r--r--src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeFeatureRenderer.java43
-rw-r--r--src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeStorage.java23
-rw-r--r--src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/SaveCapeToPlayerEntityRenderState.java19
-rw-r--r--src/main/kotlin/Compat.kt11
-rw-r--r--src/main/kotlin/Firmament.kt21
-rw-r--r--src/main/kotlin/apis/Profiles.kt6
-rw-r--r--src/main/kotlin/apis/Routes.kt16
-rw-r--r--src/main/kotlin/commands/rome.kt44
-rw-r--r--src/main/kotlin/events/CustomItemModelEvent.kt34
-rw-r--r--src/main/kotlin/events/EntityUpdateEvent.kt24
-rw-r--r--src/main/kotlin/events/PlayerInventoryUpdate.kt19
-rw-r--r--src/main/kotlin/events/WorldKeyboardEvent.kt17
-rw-r--r--src/main/kotlin/events/WorldMouseMoveEvent.kt5
-rw-r--r--src/main/kotlin/features/FeatureManager.kt29
-rw-r--r--src/main/kotlin/features/chat/ChatLinks.kt18
-rw-r--r--src/main/kotlin/features/chat/CopyChat.kt31
-rw-r--r--src/main/kotlin/features/debug/AnimatedClothingScanner.kt182
-rw-r--r--src/main/kotlin/features/debug/DebugLogger.kt2
-rw-r--r--src/main/kotlin/features/debug/DeveloperFeatures.kt53
-rw-r--r--src/main/kotlin/features/debug/ExportedTestConstantMeta.kt27
-rw-r--r--src/main/kotlin/features/debug/PowerUserTools.kt47
-rw-r--r--src/main/kotlin/features/debug/SoundVisualizer.kt65
-rw-r--r--src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt255
-rw-r--r--src/main/kotlin/features/debug/itemeditor/ItemExporter.kt184
-rw-r--r--src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt75
-rw-r--r--src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt270
-rw-r--r--src/main/kotlin/features/debug/itemeditor/PromptScreen.kt15
-rw-r--r--src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt4
-rw-r--r--src/main/kotlin/features/events/carnival/MinesweeperHelper.kt4
-rw-r--r--src/main/kotlin/features/fixes/Fixes.kt33
-rw-r--r--src/main/kotlin/features/garden/HideComposterNoises.kt32
-rw-r--r--src/main/kotlin/features/inventory/CraftingOverlay.kt2
-rw-r--r--src/main/kotlin/features/inventory/ItemHotkeys.kt5
-rw-r--r--src/main/kotlin/features/inventory/PetFeatures.kt57
-rw-r--r--src/main/kotlin/features/inventory/PriceData.kt147
-rw-r--r--src/main/kotlin/features/inventory/REIDependencyWarner.kt4
-rw-r--r--src/main/kotlin/features/inventory/SlotLocking.kt7
-rw-r--r--src/main/kotlin/features/inventory/TimerInLore.kt32
-rw-r--r--src/main/kotlin/features/inventory/WardrobeKeybinds.kt56
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButton.kt129
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt126
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButtons.kt60
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt4
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt5
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt3
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt212
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt7
-rw-r--r--src/main/kotlin/features/items/EtherwarpOverlay.kt49
-rw-r--r--src/main/kotlin/features/macros/ComboProcessor.kt114
-rw-r--r--src/main/kotlin/features/macros/HotkeyAction.kt40
-rw-r--r--src/main/kotlin/features/macros/KeyComboTrie.kt73
-rw-r--r--src/main/kotlin/features/macros/MacroData.kt12
-rw-r--r--src/main/kotlin/features/macros/MacroUI.kt285
-rw-r--r--src/main/kotlin/features/macros/RadialMenu.kt149
-rw-r--r--src/main/kotlin/features/mining/PickaxeAbility.kt16
-rw-r--r--src/main/kotlin/features/misc/CustomCapes.kt192
-rw-r--r--src/main/kotlin/features/misc/Devs.kt38
-rw-r--r--src/main/kotlin/features/misc/Hud.kt77
-rw-r--r--src/main/kotlin/features/misc/LicenseViewer.kt128
-rw-r--r--src/main/kotlin/features/world/ColeWeightCompat.kt125
-rw-r--r--src/main/kotlin/features/world/FirmWaypointManager.kt168
-rw-r--r--src/main/kotlin/features/world/FirmWaypoints.kt37
-rw-r--r--src/main/kotlin/features/world/TemporaryWaypoints.kt73
-rw-r--r--src/main/kotlin/features/world/Waypoints.kt345
-rw-r--r--src/main/kotlin/gui/config/AllConfigsGui.kt24
-rw-r--r--src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt2
-rw-r--r--src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt2
-rw-r--r--src/main/kotlin/gui/config/KeyBindingHandler.kt29
-rw-r--r--src/main/kotlin/gui/config/KeyBindingStateManager.kt44
-rw-r--r--src/main/kotlin/gui/config/ManagedConfig.kt3
-rw-r--r--src/main/kotlin/gui/config/ManagedOption.kt2
-rw-r--r--src/main/kotlin/gui/entity/EntityRenderer.kt94
-rw-r--r--src/main/kotlin/gui/entity/FakeWorld.kt343
-rw-r--r--src/main/kotlin/gui/entity/ModifyEquipment.kt8
-rw-r--r--src/main/kotlin/gui/entity/ModifyHorse.kt73
-rw-r--r--src/main/kotlin/keybindings/IKeyBinding.kt31
-rw-r--r--src/main/kotlin/keybindings/SavedKeyBinding.kt203
-rw-r--r--src/main/kotlin/repo/ExpLadder.kt3
-rw-r--r--src/main/kotlin/repo/ExpensiveItemCacheApi.kt8
-rw-r--r--src/main/kotlin/repo/HypixelStaticData.kt25
-rw-r--r--src/main/kotlin/repo/ItemCache.kt117
-rw-r--r--src/main/kotlin/repo/MiningRepoData.kt2
-rw-r--r--src/main/kotlin/repo/ModernOverlaysData.kt41
-rw-r--r--src/main/kotlin/repo/RepoDownloadManager.kt193
-rw-r--r--src/main/kotlin/repo/RepoManager.kt26
-rw-r--r--src/main/kotlin/repo/RepoModResourcePack.kt2
-rw-r--r--src/main/kotlin/repo/SBItemStack.kt54
-rw-r--r--src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt4
-rw-r--r--src/main/kotlin/repo/recipes/RecipeLayouter.kt7
-rw-r--r--src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt24
-rw-r--r--src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt76
-rw-r--r--src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt83
-rw-r--r--src/main/kotlin/util/Base64Util.kt7
-rw-r--r--src/main/kotlin/util/BazaarPriceStrategy.kt2
-rw-r--r--src/main/kotlin/util/ErrorUtil.kt25
-rw-r--r--src/main/kotlin/util/FirmFormatters.kt8
-rw-r--r--src/main/kotlin/util/HoveredItemStack.kt2
-rw-r--r--src/main/kotlin/util/LegacyTagWriter.kt103
-rw-r--r--src/main/kotlin/util/MC.kt27
-rw-r--r--src/main/kotlin/util/MoulConfigUtils.kt67
-rw-r--r--src/main/kotlin/util/SkyblockId.kt135
-rw-r--r--src/main/kotlin/util/StringUtil.kt2
-rw-r--r--src/main/kotlin/util/TestUtil.kt1
-rw-r--r--src/main/kotlin/util/asm/AsmAnnotationUtil.kt89
-rw-r--r--src/main/kotlin/util/async/input.kt108
-rw-r--r--src/main/kotlin/util/collections/RangeUtil.kt40
-rw-r--r--src/main/kotlin/util/collections/WeakCache.kt202
-rw-r--r--src/main/kotlin/util/compatloader/CompatLoader.kt2
-rw-r--r--src/main/kotlin/util/compatloader/CompatMeta.kt48
-rw-r--r--src/main/kotlin/util/data/MultiFileDataHolder.kt63
-rw-r--r--src/main/kotlin/util/json/DashlessUUIDSerializer.kt6
-rw-r--r--src/main/kotlin/util/json/KJsonUtils.kt11
-rw-r--r--src/main/kotlin/util/math/GChainReconciliation.kt102
-rw-r--r--src/main/kotlin/util/math/Projections.kt46
-rw-r--r--src/main/kotlin/util/mc/ArmorUtil.kt8
-rw-r--r--src/main/kotlin/util/mc/InitLevel.kt25
-rw-r--r--src/main/kotlin/util/mc/MCTabListAPI.kt96
-rw-r--r--src/main/kotlin/util/mc/NbtPrism.kt91
-rw-r--r--src/main/kotlin/util/mc/NbtUtil.kt10
-rw-r--r--src/main/kotlin/util/mc/PlayerUtil.kt7
-rw-r--r--src/main/kotlin/util/mc/SNbtFormatter.kt16
-rw-r--r--src/main/kotlin/util/mc/SkullItemData.kt3
-rw-r--r--src/main/kotlin/util/mc/SlotUtils.kt18
-rw-r--r--src/main/kotlin/util/mc/asFakeServer.kt37
-rw-r--r--src/main/kotlin/util/regex.kt7
-rw-r--r--src/main/kotlin/util/render/CustomRenderLayers.kt104
-rw-r--r--src/main/kotlin/util/render/DrawContextExt.kt43
-rw-r--r--src/main/kotlin/util/render/FacingThePlayerContext.kt6
-rw-r--r--src/main/kotlin/util/render/FirmamentShaders.kt18
-rw-r--r--src/main/kotlin/util/render/LerpUtils.kt35
-rw-r--r--src/main/kotlin/util/render/RenderCircleProgress.kt124
-rw-r--r--src/main/kotlin/util/render/RenderInWorldContext.kt61
-rw-r--r--src/main/kotlin/util/render/TintedOverlayTexture.kt11
-rw-r--r--src/main/kotlin/util/skyblock/SackUtil.kt2
-rw-r--r--src/main/kotlin/util/skyblock/SkyBlockItems.kt3
-rw-r--r--src/main/kotlin/util/skyblock/TabListAPI.kt41
-rw-r--r--src/main/kotlin/util/textutil.kt53
-rw-r--r--src/main/kotlin/util/uuid.kt6
-rw-r--r--src/main/resources/assets/firmament/gui/config/macros/combos.xml55
-rw-r--r--src/main/resources/assets/firmament/gui/config/macros/editor_combo.xml42
-rw-r--r--src/main/resources/assets/firmament/gui/config/macros/editor_wheel.xml43
-rw-r--r--src/main/resources/assets/firmament/gui/config/macros/index.xml27
-rw-r--r--src/main/resources/assets/firmament/gui/config/macros/wheel.xml54
-rw-r--r--src/main/resources/assets/firmament/gui/license_viewer/index.xml65
-rw-r--r--src/main/resources/assets/firmament/logo.pngbin16321 -> 19770 bytes
-rw-r--r--src/main/resources/assets/firmament/shaders/cape/parallax.fsh53
-rw-r--r--src/main/resources/assets/firmament/shaders/circle_discard_color.fsh22
-rw-r--r--src/main/resources/assets/firmament/textures/cape/REUSE.toml19
-rw-r--r--src/main/resources/assets/firmament/textures/cape/firm_static.pngbin0 -> 42249 bytes
-rw-r--r--src/main/resources/assets/firmament/textures/cape/firmament_star.pngbin0 -> 1141 bytes
-rw-r--r--src/main/resources/assets/firmament/textures/cape/fsr_static.pngbin0 -> 21747 bytes
-rw-r--r--src/main/resources/assets/firmament/textures/cape/parallax_background.pngbin0 -> 2053 bytes
-rw-r--r--src/main/resources/assets/firmament/textures/cape/parallax_template.pngbin0 -> 352 bytes
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.pngbin4766 -> 745 bytes
-rw-r--r--src/main/resources/fabric.mod.json2
-rw-r--r--src/main/resources/firmament.accesswidener9
-rw-r--r--src/main/resources/legacy_data/enchantments.json560
-rw-r--r--src/main/resources/legacy_data/items.json3733
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/inventory_button_background.pngbin0 -> 122 bytes
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png (renamed from src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png)bin639 -> 639 bytes
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png (renamed from src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png)bin147 -> 147 bytes
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta (renamed from src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta)0
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.pngbin0 -> 768 bytes
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta (renamed from src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta)0
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png (renamed from src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png)bin203 -> 203 bytes
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta (renamed from src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta)0
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png (renamed from src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png)bin795 -> 795 bytes
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta (renamed from src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta)0
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/pack.mcmeta (renamed from src/main/resources/resourcepacks/transparent_storage/pack.mcmeta)2
-rw-r--r--src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.pngbin624 -> 0 bytes
187 files changed, 11202 insertions, 1872 deletions
diff --git a/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java b/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java
index 0713068..a9db7f9 100644
--- a/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java
+++ b/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java
@@ -1,6 +1,9 @@
package moe.nea.firmament.init;
+import moe.nea.firmament.util.ErrorUtil;
+import moe.nea.firmament.util.compatloader.ICompatMeta;
+
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
@@ -94,7 +97,7 @@ public class AutoDiscoveryPlugin {
String norm = (className.substring(0, className.length() - ".class".length()))
.replace("\\", "/")
.replace("/", ".");
- if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".")) {
+ if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".") && ICompatMeta.Companion.shouldLoad(norm)) {
mixins.add(norm.substring(getMixinPackage().length() + 1));
}
}
@@ -125,24 +128,25 @@ public class AutoDiscoveryPlugin {
*/
public List<String> getMixins() {
if (mixins != null) return mixins;
- System.out.println("Trying to discover mixins");
- mixins = new ArrayList<>();
- URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation();
- System.out.println("Found classes at " + classUrl);
- tryDiscoverFromContentFile(classUrl);
- var classRoots = System.getProperty("firmament.classroots");
- if (classRoots != null && !classRoots.isBlank()) {
- System.out.println("Found firmament class roots: " + classRoots);
- for (String s : classRoots.split(File.pathSeparator)) {
- if (s.isBlank()) {
- continue;
- }
- try {
+ try {
+ System.out.println("Trying to discover mixins");
+ mixins = new ArrayList<>();
+ URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation();
+ System.out.println("Found classes at " + classUrl);
+ tryDiscoverFromContentFile(classUrl);
+ var classRoots = System.getProperty("firmament.classroots");
+ if (classRoots != null && !classRoots.isBlank()) {
+ System.out.println("Found firmament class roots: " + classRoots);
+ for (String s : classRoots.split(File.pathSeparator)) {
+ if (s.isBlank()) {
+ continue;
+ }
tryDiscoverFromContentFile(new File(s).toURI().toURL());
- } catch (MalformedURLException e) {
- throw new RuntimeException(e);
}
}
+ } catch (Exception e) {
+ e.printStackTrace();
+ System.exit(1);
}
return mixins;
}
diff --git a/src/main/java/moe/nea/firmament/init/MixinPlugin.java b/src/main/java/moe/nea/firmament/init/MixinPlugin.java
index 61e8f14..d48139b 100644
--- a/src/main/java/moe/nea/firmament/init/MixinPlugin.java
+++ b/src/main/java/moe/nea/firmament/init/MixinPlugin.java
@@ -8,54 +8,69 @@ import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class MixinPlugin implements IMixinConfigPlugin {
- AutoDiscoveryPlugin autoDiscoveryPlugin = new AutoDiscoveryPlugin();
- public static String mixinPackage;
- @Override
- public void onLoad(String mixinPackage) {
- MixinExtrasBootstrap.init();
- MixinPlugin.mixinPackage = mixinPackage;
- autoDiscoveryPlugin.setMixinPackage(mixinPackage);
- }
-
- @Override
- public String getRefMapperConfig() {
- return null;
- }
-
- @Override
- public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
- if (!Boolean.getBoolean("firmament.debug") && mixinClassName.contains("devenv.")) {
- return false;
- }
- return true;
- }
-
- @Override
- public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
-
- }
-
- @Override
- public List<String> getMixins() {
- return autoDiscoveryPlugin.getMixins().stream().filter(it -> this.shouldApplyMixin(null, it))
- .toList();
- }
-
- @Override
- public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
-
- }
-
- public static List<String> appliedMixins = new ArrayList<>();
-
- @Override
- public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
- appliedMixins.add(mixinClassName);
- }
+ AutoDiscoveryPlugin autoDiscoveryPlugin = new AutoDiscoveryPlugin();
+ public static List<MixinPlugin> instances = new ArrayList<>();
+ public String mixinPackage;
+
+ @Override
+ public void onLoad(String mixinPackage) {
+ MixinExtrasBootstrap.init();
+ instances.add(this);
+ this.mixinPackage = mixinPackage;
+ autoDiscoveryPlugin.setMixinPackage(mixinPackage);
+ }
+
+ @Override
+ public String getRefMapperConfig() {
+ return null;
+ }
+
+ @Override
+ public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
+ if (!Boolean.getBoolean("firmament.debug") && mixinClassName.contains("devenv.")) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
+
+ }
+
+ @Override
+ public List<String> getMixins() {
+ return autoDiscoveryPlugin.getMixins().stream().filter(it -> this.shouldApplyMixin(null, it))
+ .toList();
+ }
+
+ @Override
+ public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
+
+ }
+
+ public Set<String> getAppliedFullPathMixins() {
+ return new HashSet<>(appliedMixins);
+ }
+
+ public Set<String> getExpectedFullPathMixins() {
+ return getMixins()
+ .stream()
+ .map(it -> mixinPackage + "." + it)
+ .collect(Collectors.toSet());
+ }
+
+ public List<String> appliedMixins = new ArrayList<>();
+
+ @Override
+ public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
+ appliedMixins.add(mixinClassName);
+ }
}
diff --git a/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java b/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java
index f2c6c53..8b65946 100644
--- a/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java
+++ b/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java
@@ -3,10 +3,9 @@ package moe.nea.firmament.init;
import me.shedaniel.mm.api.ClassTinkerers;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.block.BlockState;
-import net.minecraft.client.render.block.BlockModels;
import net.minecraft.client.render.block.BlockRenderManager;
import net.minecraft.client.render.chunk.SectionBuilder;
-import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.BlockStateModel;
import net.minecraft.util.math.BlockPos;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
@@ -20,98 +19,100 @@ import org.objectweb.asm.tree.VarInsnNode;
public class SectionBuilderRiser extends RiserUtils {
- @IntermediaryName(SectionBuilder.class)
- String SectionBuilder;
- @IntermediaryName(BlockPos.class)
- String BlockPos;
- @IntermediaryName(BlockRenderManager.class)
- String BlockRenderManager;
- @IntermediaryName(BlockState.class)
- String BlockState;
- @IntermediaryName(BakedModel.class)
- String BakedModel;
- String CustomBlockTextures = "moe.nea.firmament.features.texturepack.CustomBlockTextures";
+ @IntermediaryName(SectionBuilder.class)
+ String SectionBuilder;
+ @IntermediaryName(BlockPos.class)
+ String BlockPos;
+ @IntermediaryName(BlockRenderManager.class)
+ String BlockRenderManager;
+ @IntermediaryName(BlockState.class)
+ String BlockState;
+ @IntermediaryName(BlockStateModel.class)
+ String BlockStateModel;
+ String CustomBlockTextures = "moe.nea.firmament.features.texturepack.CustomBlockTextures";
- Type getModelDesc = Type.getMethodType(
- getTypeForClassName(BlockRenderManager),
- getTypeForClassName(BlockState)
- );
- String getModel = remapper.mapMethodName(
- "intermediary",
- Intermediary.<BlockRenderManager>className(),
- Intermediary.methodName(net.minecraft.client.render.block.BlockRenderManager::getModel),
- Type.getMethodDescriptor(
- getTypeForClassName(Intermediary.<BakedModel>className()),
- getTypeForClassName(Intermediary.<BlockState>className())
- )
- );
+ Type getModelDesc = Type.getMethodType(
+ getTypeForClassName(BlockRenderManager),
+ getTypeForClassName(BlockState)
+ );
+ String getModel = remapper.mapMethodName(
+ "intermediary",
+ Intermediary.<BlockRenderManager>className(),
+ Intermediary.methodName(net.minecraft.client.render.block.BlockRenderManager::getModel),
+ Type.getMethodDescriptor(
+ getTypeForClassName(Intermediary.<BlockStateModel>className()),
+ getTypeForClassName(Intermediary.<BlockState>className())
+ )
+ );
- @Override
- public void addTinkerers() {
- if (FabricLoader.getInstance().isModLoaded("fabric-renderer-indigo"))
- ClassTinkerers.addTransformation(SectionBuilder, this::handle, true);
- }
+ @Override
+ public void addTinkerers() {
+ if (FabricLoader.getInstance().isModLoaded("fabric-renderer-indigo"))
+ ClassTinkerers.addTransformation(SectionBuilder, this::handle, true);
+ }
- private void handle(ClassNode classNode) {
- for (MethodNode method : classNode.methods) {
- if ((method.name.endsWith("$fabric-renderer-indigo$hookBuildRenderBlock")
- || method.name.endsWith("$fabric-renderer-indigo$hookChunkBuildTessellate")) &&
- method.name.startsWith("redirect$")) {
- handleIndigo(method);
- return;
- }
- }
- System.err.println("Could not inject indigo rendering hook. Is a custom renderer installed (e.g. sodium)?");
- }
+ private void handle(ClassNode classNode) {
+ System.out.println("AVAST! "+ getModel);
+ for (MethodNode method : classNode.methods) {
+ if ((method.name.endsWith("$fabric-renderer-indigo$hookBuildRenderBlock")
+ || method.name.endsWith("$fabric-renderer-indigo$hookChunkBuildTessellate")) &&
+ method.name.startsWith("redirect$")) {
+ handleIndigo(method);
+ return;
+ }
+ }
+ System.err.println("Could not inject indigo rendering hook. Is a custom renderer installed (e.g. sodium)?");
+ }
- private void handleIndigo(MethodNode method) {
- LocalVariableNode blockPosVar = null, blockStateVar = null;
- for (LocalVariableNode localVariable : method.localVariables) {
- if (Type.getType(localVariable.desc).equals(getTypeForClassName(BlockPos))) {
- blockPosVar = localVariable;
- }
- if (Type.getType(localVariable.desc).equals(getTypeForClassName(BlockState))) {
- blockStateVar = localVariable;
- }
- }
- if (blockPosVar == null || blockStateVar == null) {
- System.err.println("Firmament could inject into indigo: missing either block pos or blockstate");
- return;
- }
- for (AbstractInsnNode instruction : method.instructions) {
- if (instruction.getOpcode() != Opcodes.INVOKEVIRTUAL) continue;
- var methodInsn = (MethodInsnNode) instruction;
- if (!(methodInsn.name.equals(getModel) && Type.getObjectType(methodInsn.owner).equals(getTypeForClassName(BlockRenderManager))))
- continue;
- method.instructions.insertBefore(
- methodInsn,
- new MethodInsnNode(
- Opcodes.INVOKESTATIC,
- getTypeForClassName(CustomBlockTextures).getInternalName(),
- "enterFallbackCall",
- Type.getMethodDescriptor(Type.VOID_TYPE)
- ));
+ private void handleIndigo(MethodNode method) {
+ LocalVariableNode blockPosVar = null, blockStateVar = null;
+ for (LocalVariableNode localVariable : method.localVariables) {
+ if (Type.getType(localVariable.desc).equals(getTypeForClassName(BlockPos))) {
+ blockPosVar = localVariable;
+ }
+ if (Type.getType(localVariable.desc).equals(getTypeForClassName(BlockState))) {
+ blockStateVar = localVariable;
+ }
+ }
+ if (blockPosVar == null || blockStateVar == null) {
+ System.err.println("Firmament could inject into indigo: missing either block pos or blockstate");
+ return;
+ }
+ for (AbstractInsnNode instruction : method.instructions) {
+ if (instruction.getOpcode() != Opcodes.INVOKEVIRTUAL) continue;
+ var methodInsn = (MethodInsnNode) instruction;
+ if (!(methodInsn.name.equals(getModel) && Type.getObjectType(methodInsn.owner).equals(getTypeForClassName(BlockRenderManager))))
+ continue;
+ method.instructions.insertBefore(
+ methodInsn,
+ new MethodInsnNode(
+ Opcodes.INVOKESTATIC,
+ getTypeForClassName(CustomBlockTextures).getInternalName(),
+ "enterFallbackCall",
+ Type.getMethodDescriptor(Type.VOID_TYPE)
+ ));
- var insnList = new InsnList();
- insnList.add(new MethodInsnNode(
- Opcodes.INVOKESTATIC,
- getTypeForClassName(CustomBlockTextures).getInternalName(),
- "exitFallbackCall",
- Type.getMethodDescriptor(Type.VOID_TYPE)
- ));
- insnList.add(new VarInsnNode(Opcodes.ALOAD, blockPosVar.index));
- insnList.add(new VarInsnNode(Opcodes.ALOAD, blockStateVar.index));
- insnList.add(new MethodInsnNode(
- Opcodes.INVOKESTATIC,
- getTypeForClassName(CustomBlockTextures).getInternalName(),
- "patchIndigo",
- Type.getMethodDescriptor(getTypeForClassName(BakedModel),
- getTypeForClassName(BakedModel),
- getTypeForClassName(BlockPos),
- getTypeForClassName(BlockState)),
- false
- ));
- method.instructions.insert(methodInsn, insnList);
- }
- }
+ var insnList = new InsnList();
+ insnList.add(new MethodInsnNode(
+ Opcodes.INVOKESTATIC,
+ getTypeForClassName(CustomBlockTextures).getInternalName(),
+ "exitFallbackCall",
+ Type.getMethodDescriptor(Type.VOID_TYPE)
+ ));
+ insnList.add(new VarInsnNode(Opcodes.ALOAD, blockPosVar.index));
+ insnList.add(new VarInsnNode(Opcodes.ALOAD, blockStateVar.index));
+ insnList.add(new MethodInsnNode(
+ Opcodes.INVOKESTATIC,
+ getTypeForClassName(CustomBlockTextures).getInternalName(),
+ "patchIndigo",
+ Type.getMethodDescriptor(
+ getTypeForClassName(BlockStateModel),
+ getTypeForClassName(BlockStateModel),
+ getTypeForClassName(BlockPos),
+ getTypeForClassName(BlockState)),
+ false
+ ));
+ method.instructions.insert(methodInsn, insnList);
+ }
+ }
}
diff --git a/src/main/java/moe/nea/firmament/mixins/CopyChatPatch.java b/src/main/java/moe/nea/firmament/mixins/CopyChatPatch.java
new file mode 100644
index 0000000..6996818
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/CopyChatPatch.java
@@ -0,0 +1,44 @@
+package moe.nea.firmament.mixins;
+
+import moe.nea.firmament.features.chat.CopyChat;
+import moe.nea.firmament.mixins.accessor.AccessorChatHud;
+import moe.nea.firmament.util.ClipboardUtils;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.hud.ChatHud;
+import net.minecraft.client.gui.hud.ChatHudLine;
+import net.minecraft.client.gui.screen.ChatScreen;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.math.MathHelper;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+import java.util.List;
+
+@Mixin(ChatScreen.class)
+public class CopyChatPatch {
+ @Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true)
+ private void onRightClick(double mouseX, double mouseY, int button, CallbackInfoReturnable<Boolean> cir) throws NoSuchFieldException, IllegalAccessException {
+ if (button != 1 || !CopyChat.TConfig.INSTANCE.getCopyChat()) return;
+ MinecraftClient client = MinecraftClient.getInstance();
+ ChatHud chatHud = client.inGameHud.getChatHud();
+ int lineIndex = getChatLineIndex(chatHud, mouseY);
+ if (lineIndex < 0) return;
+ List<ChatHudLine.Visible> visible = ((AccessorChatHud) chatHud).getVisibleMessages_firmament();
+ if (lineIndex >= visible.size()) return;
+ ChatHudLine.Visible line = visible.get(lineIndex);
+ String text = CopyChat.INSTANCE.orderedTextToString(line.content());
+ ClipboardUtils.INSTANCE.setTextContent(text);
+ chatHud.addMessage(Text.literal("Copied: ").append(text).formatted(Formatting.GRAY));
+ cir.setReturnValue(true);
+ cir.cancel();
+ }
+
+ @Unique
+ private int getChatLineIndex(ChatHud chatHud, double mouseY) {
+ double chatLineY = ((AccessorChatHud) chatHud).toChatLineY_firmament(mouseY);
+ return MathHelper.floor(chatLineY + ((AccessorChatHud) chatHud).getScrolledLines_firmament());
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/DispatchMouseInputEventsPatch.java b/src/main/java/moe/nea/firmament/mixins/DispatchMouseInputEventsPatch.java
new file mode 100644
index 0000000..f1b07bb
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/DispatchMouseInputEventsPatch.java
@@ -0,0 +1,17 @@
+package moe.nea.firmament.mixins;
+
+import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
+import moe.nea.firmament.events.WorldMouseMoveEvent;
+import net.minecraft.client.Mouse;
+import net.minecraft.client.network.ClientPlayerEntity;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin(Mouse.class)
+public class DispatchMouseInputEventsPatch {
+ @WrapWithCondition(method = "updateMouse", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;changeLookDirection(DD)V"))
+ public boolean onRotatePlayer(ClientPlayerEntity instance, double deltaX, double deltaY) {
+ var event = WorldMouseMoveEvent.Companion.publish(new WorldMouseMoveEvent(deltaX, deltaY));
+ return !event.getCancelled();
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/HudRenderEventsPatch.java b/src/main/java/moe/nea/firmament/mixins/HudRenderEventsPatch.java
index 85c0462..49e86fb 100644
--- a/src/main/java/moe/nea/firmament/mixins/HudRenderEventsPatch.java
+++ b/src/main/java/moe/nea/firmament/mixins/HudRenderEventsPatch.java
@@ -4,6 +4,7 @@ package moe.nea.firmament.mixins;
import moe.nea.firmament.events.HotbarItemRenderEvent;
import moe.nea.firmament.events.HudRenderEvent;
+import moe.nea.firmament.features.fixes.Fixes;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.hud.InGameHud;
import net.minecraft.client.render.RenderTickCounter;
@@ -26,4 +27,10 @@ public class HudRenderEventsPatch {
if (stack != null && !stack.isEmpty())
HotbarItemRenderEvent.Companion.publish(new HotbarItemRenderEvent(stack, context, x, y, tickCounter));
}
+
+ @Inject(method = "renderStatusEffectOverlay", at = @At("HEAD"), cancellable = true)
+ public void hideStatusEffects(CallbackInfo ci) {
+ if (Fixes.TConfig.INSTANCE.getHidePotionEffectsHud()) ci.cancel();
+ }
+
}
diff --git a/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java b/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java
index 48f3c23..d2b3f91 100644
--- a/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java
+++ b/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java
@@ -2,18 +2,19 @@
package moe.nea.firmament.mixins;
+import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import moe.nea.firmament.events.WorldKeyboardEvent;
import net.minecraft.client.Keyboard;
+import net.minecraft.client.util.InputUtil;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.Inject;
-import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Keyboard.class)
public class KeyPressInWorldEventPatch {
- @Inject(method = "onKey", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/option/KeyBinding;onKeyPressed(Lnet/minecraft/client/util/InputUtil$Key;)V"))
- public void onKeyBoardInWorld(long window, int key, int scancode, int action, int modifiers, CallbackInfo ci) {
- WorldKeyboardEvent.Companion.publish(new WorldKeyboardEvent(key, scancode, modifiers));
- }
+ @WrapWithCondition(method = "onKey", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/option/KeyBinding;onKeyPressed(Lnet/minecraft/client/util/InputUtil$Key;)V"))
+ public boolean onKeyBoardInWorld(InputUtil.Key key, long window, int _key, int scancode, int action, int modifiers) {
+ var event = WorldKeyboardEvent.Companion.publish(new WorldKeyboardEvent(_key, scancode, modifiers));
+ return !event.getCancelled();
+ }
}
diff --git a/src/main/java/moe/nea/firmament/mixins/MinecraftInitLevelListener.java b/src/main/java/moe/nea/firmament/mixins/MinecraftInitLevelListener.java
new file mode 100644
index 0000000..1673987
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/MinecraftInitLevelListener.java
@@ -0,0 +1,26 @@
+package moe.nea.firmament.mixins;
+
+import moe.nea.firmament.util.mc.InitLevel;
+import net.minecraft.client.MinecraftClient;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(MinecraftClient.class)
+public class MinecraftInitLevelListener {
+ @Inject(method = "<init>", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;initBackendSystem()Lnet/minecraft/util/TimeSupplier$Nanoseconds;"))
+ private void onInitRenderBackend(CallbackInfo ci) {
+ InitLevel.bump(InitLevel.RENDER_INIT);
+ }
+
+ @Inject(method = "<init>", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;initRenderer(JIZLjava/util/function/BiFunction;Z)V"))
+ private void onInitRender(CallbackInfo ci) {
+ InitLevel.bump(InitLevel.RENDER);
+ }
+
+ @Inject(method = "<init>", at = @At(value = "TAIL"))
+ private void onFinishedLoading(CallbackInfo ci) {
+ InitLevel.bump(InitLevel.MAIN_MENU);
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/MixinRecipeBookScreen.java b/src/main/java/moe/nea/firmament/mixins/MixinRecipeBookScreen.java
new file mode 100644
index 0000000..2dbe738
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/MixinRecipeBookScreen.java
@@ -0,0 +1,16 @@
+package moe.nea.firmament.mixins;
+
+import moe.nea.firmament.features.fixes.Fixes;
+import net.minecraft.client.gui.screen.ingame.RecipeBookScreen;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(value = RecipeBookScreen.class, priority = 999)
+public class MixinRecipeBookScreen {
+ @Inject(method = "addRecipeBook", at = @At("HEAD"), cancellable = true)
+ public void addRecipeBook(CallbackInfo ci) {
+ if (Fixes.TConfig.INSTANCE.getHideRecipeBook()) ci.cancel();
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java b/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java
index b20c223..f07604e 100644
--- a/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java
+++ b/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java
@@ -20,7 +20,7 @@ public abstract class PlayerDropEventPatch extends PlayerEntity {
@Inject(method = "dropSelectedItem", at = @At("HEAD"), cancellable = true)
public void onDropSelectedItem(boolean entireStack, CallbackInfoReturnable<Boolean> cir) {
- Slot fakeSlot = new Slot(getInventory(), getInventory().selectedSlot, 0, 0);
+ Slot fakeSlot = new Slot(getInventory(), getInventory().getSelectedSlot(), 0, 0);
if (IsSlotProtectedEvent.shouldBlockInteraction(fakeSlot, SlotActionType.THROW, IsSlotProtectedEvent.MoveOrigin.DROP_FROM_HOTBAR)) {
cir.setReturnValue(false);
}
diff --git a/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java b/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java
index 06ecbd4..a4ae931 100644
--- a/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java
+++ b/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java
@@ -43,11 +43,11 @@ public abstract class SlotUpdateListener extends ClientCommonNetworkHandler {
private void onMultiSlotUpdate(InventoryS2CPacket packet, CallbackInfo ci) {
var player = this.client.player;
assert player != null;
- if (packet.getSyncId() == 0) {
- PlayerInventoryUpdate.Companion.publish(new PlayerInventoryUpdate.Multi(packet.getContents()));
- } else if (packet.getSyncId() == player.currentScreenHandler.syncId) {
+ if (packet.syncId() == 0) {
+ PlayerInventoryUpdate.Companion.publish(new PlayerInventoryUpdate.Multi(packet.contents()));
+ } else if (packet.syncId() == player.currentScreenHandler.syncId) {
ChestInventoryUpdateEvent.Companion.publish(
- new ChestInventoryUpdateEvent.Multi(packet.getContents())
+ new ChestInventoryUpdateEvent.Multi(packet.contents())
);
}
}
diff --git a/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java b/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java
index 5c52d70..b8cba80 100644
--- a/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java
+++ b/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java
@@ -1,30 +1,32 @@
package moe.nea.firmament.mixins;
+import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import moe.nea.firmament.events.SoundReceiveEvent;
import net.minecraft.client.network.ClientPlayNetworkHandler;
-import net.minecraft.network.packet.s2c.play.PlaySoundS2CPacket;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.entity.Entity;
+import net.minecraft.registry.entry.RegistryEntry;
+import net.minecraft.sound.SoundCategory;
+import net.minecraft.sound.SoundEvent;
import net.minecraft.util.math.Vec3d;
+import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.Inject;
-import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ClientPlayNetworkHandler.class)
public class SoundReceiveEventPatch {
- @Inject(method = "onPlaySound", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/world/ClientWorld;playSound(Lnet/minecraft/entity/player/PlayerEntity;DDDLnet/minecraft/registry/entry/RegistryEntry;Lnet/minecraft/sound/SoundCategory;FFJ)V"), cancellable = true)
- private void postEventWhenSoundIsPlayed(PlaySoundS2CPacket packet, CallbackInfo ci) {
+ @WrapWithCondition(method = "onPlaySound", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/world/ClientWorld;playSound(Lnet/minecraft/entity/Entity;DDDLnet/minecraft/registry/entry/RegistryEntry;Lnet/minecraft/sound/SoundCategory;FFJ)V"))
+ private boolean postEventWhenSoundIsPlayed(ClientWorld instance, @Nullable Entity source, double x, double y, double z, RegistryEntry<SoundEvent> sound, SoundCategory category, float volume, float pitch, long seed) {
var event = new SoundReceiveEvent(
- packet.getSound(),
- packet.getCategory(),
- new Vec3d(packet.getX(), packet.getY(), packet.getZ()),
- packet.getPitch(),
- packet.getVolume(),
- packet.getSeed()
+ sound,
+ category,
+ new Vec3d(x,y,z),
+ pitch,
+ volume,
+ seed
);
SoundReceiveEvent.Companion.publish(event);
- if (event.getCancelled()) {
- ci.cancel();
- }
+ return !event.getCancelled();
}
}
diff --git a/src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java b/src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java
index 847fb4d..3ed8c1b 100644
--- a/src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java
+++ b/src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java
@@ -2,11 +2,9 @@
package moe.nea.firmament.mixins;
-import com.llamalad7.mixinextras.sugar.Local;
import moe.nea.firmament.events.WorldRenderLastEvent;
import net.minecraft.client.render.*;
import net.minecraft.client.util.Handle;
-import net.minecraft.client.util.ObjectAllocator;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.profiler.Profiler;
import org.joml.Matrix4f;
@@ -31,12 +29,12 @@ public abstract class WorldRenderLastEventPatch {
protected abstract void checkEmpty(MatrixStack matrices);
@Inject(method = "method_62214", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/profiler/Profiler;pop()V", shift = At.Shift.AFTER))
- public void onWorldRenderLast(Fog fog, RenderTickCounter tickCounter, Camera camera, Profiler profiler, Matrix4f matrix4f, Matrix4f matrix4f2, Handle handle, Handle handle2, Handle handle3, Handle handle4, boolean bl, Frustum frustum, Handle handle5, CallbackInfo ci) {
+ public void onWorldRenderLast(Fog fog, RenderTickCounter renderTickCounter, Camera camera, Profiler profiler, Matrix4f matrix4f, Matrix4f matrix4f2, Handle handle, Handle handle2, boolean bl, Frustum frustum, Handle handle3, Handle handle4, CallbackInfo ci) {
var imm = this.bufferBuilders.getEntityVertexConsumers();
var stack = new MatrixStack();
// TODO: pre-cancel this event if F1 is active
var event = new WorldRenderLastEvent(
- stack, tickCounter,
+ stack, renderTickCounter,
camera,
imm
);
diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorChatHud.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorChatHud.java
index 72a72f0..d164aac 100644
--- a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorChatHud.java
+++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorChatHud.java
@@ -4,6 +4,7 @@ import net.minecraft.client.gui.hud.ChatHud;
import net.minecraft.client.gui.hud.ChatHudLine;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
+import org.spongepowered.asm.mixin.gen.Invoker;
import java.util.List;
@@ -11,4 +12,13 @@ import java.util.List;
public interface AccessorChatHud {
@Accessor("messages")
List<ChatHudLine> getMessages_firmament();
+
+ @Accessor("visibleMessages")
+ List<ChatHudLine.Visible> getVisibleMessages_firmament();
+
+ @Accessor("scrolledLines")
+ int getScrolledLines_firmament();
+
+ @Invoker("toChatLineY")
+ double toChatLineY_firmament(double y);
}
diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java
index 7ed04b1..f55ef4f 100644
--- a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java
+++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java
@@ -1,5 +1,3 @@
-
-
package moe.nea.firmament.mixins.accessor;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorPlayerListHud.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorPlayerListHud.java
new file mode 100644
index 0000000..81ea0fd
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorPlayerListHud.java
@@ -0,0 +1,31 @@
+package moe.nea.firmament.mixins.accessor;
+
+import net.minecraft.client.gui.hud.PlayerListHud;
+import net.minecraft.client.network.PlayerListEntry;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+import org.spongepowered.asm.mixin.gen.Invoker;
+
+import java.util.Comparator;
+import java.util.List;
+
+@Mixin(PlayerListHud.class)
+public interface AccessorPlayerListHud {
+
+ @Accessor("ENTRY_ORDERING")
+ static Comparator<PlayerListEntry> getEntryOrdering() {
+ throw new AssertionError();
+ }
+
+ @Invoker("collectPlayerEntries")
+ List<PlayerListEntry> collectPlayerEntries_firmament();
+
+ @Accessor("footer")
+ @Nullable Text getFooter_firmament();
+
+ @Accessor("header")
+ @Nullable Text getHeader_firmament();
+
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/feature/DisableSlotHighlights.java b/src/main/java/moe/nea/firmament/mixins/feature/DisableSlotHighlights.java
new file mode 100644
index 0000000..0abed22
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/feature/DisableSlotHighlights.java
@@ -0,0 +1,25 @@
+package moe.nea.firmament.mixins.feature;
+
+import moe.nea.firmament.features.fixes.Fixes;
+import net.minecraft.component.DataComponentTypes;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+@Mixin(Slot.class)
+public abstract class DisableSlotHighlights {
+ @Shadow
+ public abstract ItemStack getStack();
+
+ @Inject(method = "canBeHighlighted", at = @At("HEAD"), cancellable = true)
+ private void dontHighlight(CallbackInfoReturnable<Boolean> cir) {
+ if (!Fixes.TConfig.INSTANCE.getHideSlotHighlights()) return;
+ var display = getStack().get(DataComponentTypes.TOOLTIP_DISPLAY);
+ if (display != null && display.hideTooltip())
+ cir.setReturnValue(false);
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeFeatureRenderer.java b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeFeatureRenderer.java
new file mode 100644
index 0000000..5a92f89
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeFeatureRenderer.java
@@ -0,0 +1,43 @@
+package moe.nea.firmament.mixins.feature.devcosmetics;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import com.llamalad7.mixinextras.sugar.Local;
+import kotlin.Unit;
+import moe.nea.firmament.features.misc.CustomCapes;
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.client.render.VertexConsumer;
+import net.minecraft.client.render.VertexConsumerProvider;
+import net.minecraft.client.render.entity.feature.CapeFeatureRenderer;
+import net.minecraft.client.render.entity.feature.FeatureRenderer;
+import net.minecraft.client.render.entity.feature.FeatureRendererContext;
+import net.minecraft.client.render.entity.model.BipedEntityModel;
+import net.minecraft.client.render.entity.model.PlayerEntityModel;
+import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
+import net.minecraft.client.util.SkinTextures;
+import net.minecraft.client.util.math.MatrixStack;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin(CapeFeatureRenderer.class)
+public abstract class CustomCapeFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> {
+ public CustomCapeFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context) {
+ super(context);
+ }
+
+ @WrapOperation(
+ method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/render/entity/state/PlayerEntityRenderState;FF)V",
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/entity/model/BipedEntityModel;render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumer;II)V")
+ )
+ private void onRender(BipedEntityModel instance, MatrixStack matrixStack, VertexConsumer vertexConsumer, int light, int overlay, Operation<Void> original, @Local PlayerEntityRenderState playerEntityRenderState, @Local SkinTextures skinTextures, @Local VertexConsumerProvider vertexConsumerProvider) {
+ CustomCapes.render(
+ playerEntityRenderState,
+ vertexConsumer,
+ RenderLayer.getEntitySolid(skinTextures.capeTexture()),
+ vertexConsumerProvider,
+ updatedConsumer -> {
+ original.call(instance, matrixStack, updatedConsumer, light, overlay);
+ return Unit.INSTANCE;
+ });
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeStorage.java b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeStorage.java
new file mode 100644
index 0000000..428d7ec
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeStorage.java
@@ -0,0 +1,23 @@
+package moe.nea.firmament.mixins.feature.devcosmetics;
+
+import moe.nea.firmament.features.misc.CustomCapes;
+import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+
+@Mixin(PlayerEntityRenderState.class)
+public class CustomCapeStorage implements CustomCapes.CapeStorage {
+ @Unique
+ CustomCapes.CustomCape customCape;
+
+ @Override
+ public CustomCapes.@Nullable CustomCape getCape_firmament() {
+ return customCape;
+ }
+
+ @Override
+ public void setCape_firmament(CustomCapes.@Nullable CustomCape customCape) {
+ this.customCape = customCape;
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/SaveCapeToPlayerEntityRenderState.java b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/SaveCapeToPlayerEntityRenderState.java
new file mode 100644
index 0000000..ae9c743
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/SaveCapeToPlayerEntityRenderState.java
@@ -0,0 +1,19 @@
+package moe.nea.firmament.mixins.feature.devcosmetics;
+
+import moe.nea.firmament.features.misc.CustomCapes;
+import net.minecraft.client.network.AbstractClientPlayerEntity;
+import net.minecraft.client.render.entity.PlayerEntityRenderer;
+import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(PlayerEntityRenderer.class)
+public class SaveCapeToPlayerEntityRenderState {
+ @Inject(method = "updateRenderState(Lnet/minecraft/client/network/AbstractClientPlayerEntity;Lnet/minecraft/client/render/entity/state/PlayerEntityRenderState;F)V",
+ at = @At("TAIL"))
+ private void addCustomCape(AbstractClientPlayerEntity abstractClientPlayerEntity, PlayerEntityRenderState playerEntityRenderState, float f, CallbackInfo ci) {
+ CustomCapes.addCapeData(abstractClientPlayerEntity, playerEntityRenderState);
+ }
+}
diff --git a/src/main/kotlin/Compat.kt b/src/main/kotlin/Compat.kt
new file mode 100644
index 0000000..ba3c88d
--- /dev/null
+++ b/src/main/kotlin/Compat.kt
@@ -0,0 +1,11 @@
+package moe.nea.firmament
+
+import moe.nea.firmament.util.compatloader.CompatMeta
+import moe.nea.firmament.util.compatloader.ICompatMeta
+
+@CompatMeta
+object Compat : ICompatMeta {
+ override fun shouldLoad(): Boolean {
+ return true
+ }
+}
diff --git a/src/main/kotlin/Firmament.kt b/src/main/kotlin/Firmament.kt
index 0191036..b00546a 100644
--- a/src/main/kotlin/Firmament.kt
+++ b/src/main/kotlin/Firmament.kt
@@ -51,6 +51,7 @@ import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.data.IDataHolder
+import moe.nea.firmament.util.mc.InitLevel
import moe.nea.firmament.util.tr
object Firmament {
@@ -66,6 +67,8 @@ object Firmament {
}
val version: Version by lazy { metadata.version }
+ private val DEFAULT_JSON_INDENT = " "
+
@OptIn(ExperimentalSerializationApi::class)
val json = Json {
prettyPrint = DEBUG
@@ -73,10 +76,23 @@ object Firmament {
allowTrailingComma = true
ignoreUnknownKeys = true
encodeDefaults = true
+ prettyPrintIndent = if (prettyPrint) "\t" else DEFAULT_JSON_INDENT
+ }
+
+ /**
+ * FUCK two space indentation
+ */
+ val twoSpaceJson = Json(from = json) {
+ prettyPrint = true
+ prettyPrintIndent = " "
}
val gson = Gson()
val tightJson = Json(from = json) {
prettyPrint = false
+ // Reset pretty print indent back to default to prevent getting yelled at by json
+ prettyPrintIndent = DEFAULT_JSON_INDENT
+ encodeDefaults = false
+ explicitNulls = false
}
@@ -119,6 +135,7 @@ object Firmament {
@JvmStatic
fun onClientInitialize() {
+ InitLevel.bump(InitLevel.MC_INIT)
FeatureManager.subscribeEvents()
ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { instance ->
TickEvent.publish(TickEvent(MC.currentTick++))
@@ -148,9 +165,9 @@ object Firmament {
})
ClientInitEvent.publish(ClientInitEvent())
ResourceManagerHelper.registerBuiltinResourcePack(
- identifier("transparent_storage"),
+ identifier("transparent_overlay"),
modContainer,
- tr("firmament.resourcepack.transparentstorage", "Transparent Firmament Storage Overlay"),
+ tr("firmament.resourcepack.transparentoverlay", "Transparent Firmament Overlay"),
ResourcePackActivationType.NORMAL
)
}
diff --git a/src/main/kotlin/apis/Profiles.kt b/src/main/kotlin/apis/Profiles.kt
index 789364a..156de89 100644
--- a/src/main/kotlin/apis/Profiles.kt
+++ b/src/main/kotlin/apis/Profiles.kt
@@ -188,7 +188,7 @@ data class PlayerData(
}
@Serializable
-data class AshconNameLookup(
- val username: String,
- val uuid: UUID,
+data class MowojangNameLookup(
+ val name: String,
+ val id: UUID,
)
diff --git a/src/main/kotlin/apis/Routes.kt b/src/main/kotlin/apis/Routes.kt
index bf55a2d..5e29402 100644
--- a/src/main/kotlin/apis/Routes.kt
+++ b/src/main/kotlin/apis/Routes.kt
@@ -28,13 +28,13 @@ object Routes {
return withContext(MinecraftDispatcher) {
UUIDToName.computeIfAbsent(uuid) {
async(Firmament.coroutineScope.coroutineContext) {
- val response = Firmament.httpClient.get("https://api.ashcon.app/mojang/v2/user/$uuid")
+ val response = Firmament.httpClient.get("https://mowojang.matdoes.dev/$uuid")
if (!response.status.isSuccess()) return@async null
- val data = response.body<AshconNameLookup>()
+ val data = response.body<MowojangNameLookup>()
launch(MinecraftDispatcher) {
- nameToUUID[data.username] = async { data.uuid }
+ nameToUUID[data.name] = async { data.id }
}
- data.username
+ data.name
}
}
}.await()
@@ -44,13 +44,13 @@ object Routes {
return withContext(MinecraftDispatcher) {
nameToUUID.computeIfAbsent(name) {
async(Firmament.coroutineScope.coroutineContext) {
- val response = Firmament.httpClient.get("https://api.ashcon.app/mojang/v2/user/$name")
+ val response = Firmament.httpClient.get("https://mowojang.matdoes.dev/$name")
if (!response.status.isSuccess()) return@async null
- val data = response.body<AshconNameLookup>()
+ val data = response.body<MowojangNameLookup>()
launch(MinecraftDispatcher) {
- UUIDToName[data.uuid] = async { data.username }
+ UUIDToName[data.id] = async { data.name }
}
- data.uuid
+ data.id
}
}
}.await()
diff --git a/src/main/kotlin/commands/rome.kt b/src/main/kotlin/commands/rome.kt
index 8ae34f6..f808231 100644
--- a/src/main/kotlin/commands/rome.kt
+++ b/src/main/kotlin/commands/rome.kt
@@ -1,6 +1,7 @@
package moe.nea.firmament.commands
import com.mojang.brigadier.CommandDispatcher
+import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.arguments.StringArgumentType.string
import io.ktor.client.statement.bodyAsText
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
@@ -11,6 +12,7 @@ import moe.nea.firmament.apis.UrsaManager
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.FirmamentEventBus
import moe.nea.firmament.features.debug.DebugLogger
+import moe.nea.firmament.features.debug.DeveloperFeatures
import moe.nea.firmament.features.debug.PowerUserTools
import moe.nea.firmament.features.inventory.buttons.InventoryButtons
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen
@@ -33,6 +35,7 @@ import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.ScreenUtil
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.accessors.messages
+import moe.nea.firmament.util.asBazaarStock
import moe.nea.firmament.util.collections.InstanceList
import moe.nea.firmament.util.collections.WeakCache
import moe.nea.firmament.util.mc.SNbtFormatter
@@ -130,6 +133,15 @@ fun firmamentCommand() = literal("firmament") {
}
}
thenLiteral("repo") {
+ thenLiteral("checkpr") {
+ thenArgument("prnum", IntegerArgumentType.integer(1)) { prnum ->
+ thenExecute {
+ val prnum = this[prnum]
+ source.sendFeedback(tr("firmament.repo.reload.pr", "Temporarily reloading repo from PR #${prnum}."))
+ RepoManager.downloadOverridenBranch("refs/pull/$prnum/head")
+ }
+ }
+ }
thenLiteral("reload") {
thenLiteral("fetch") {
thenExecute {
@@ -149,7 +161,7 @@ fun firmamentCommand() = literal("firmament") {
thenExecute {
val itemName = SkyblockId(get(item))
source.sendFeedback(Text.stringifiedTranslatable("firmament.price", itemName.neuItem))
- val bazaarData = HypixelStaticData.bazaarData[itemName]
+ val bazaarData = HypixelStaticData.bazaarData[itemName.asBazaarStock]
if (bazaarData != null) {
source.sendFeedback(Text.translatable("firmament.price.bazaar"))
source.sendFeedback(
@@ -192,7 +204,7 @@ fun firmamentCommand() = literal("firmament") {
}
}
}
- thenLiteral("dev") {
+ thenLiteral(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
thenLiteral("simulate") {
thenArgument("message", RestArgumentType) { message ->
thenExecute {
@@ -218,6 +230,15 @@ fun firmamentCommand() = literal("firmament") {
}
}
}
+ thenLiteral("screens") {
+ thenExecute {
+ MC.sendChat(Text.literal("""
+ |Screen: ${MC.screen} (${MC.screen?.title})
+ |Screen Handler: ${MC.handledScreen?.screenHandler} ${MC.handledScreen?.screenHandler?.syncId}
+ |Player Screen Handler: ${MC.player?.currentScreenHandler} ${MC.player?.currentScreenHandler?.syncId}
+ """.trimMargin()))
+ }
+ }
thenLiteral("blocks") {
thenExecute {
ScreenUtil.setScreenLater(MiningBlockInfoUi.makeScreen())
@@ -252,7 +273,8 @@ fun firmamentCommand() = literal("firmament") {
source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.gametype", locrawInfo.gametype))
source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.mode", locrawInfo.mode))
source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.map", locrawInfo.map))
- source.sendFeedback(tr("firmament.sbinfo.custommining", "Custom Mining: ${formatBool(locrawInfo.skyblockLocation?.hasCustomMining ?: false)}"))
+ source.sendFeedback(tr("firmament.sbinfo.custommining",
+ "Custom Mining: ${formatBool(locrawInfo.skyblockLocation?.hasCustomMining ?: false)}"))
}
}
}
@@ -303,13 +325,15 @@ fun firmamentCommand() = literal("firmament") {
}
thenLiteral("mixins") {
thenExecute {
- source.sendFeedback(Text.translatable("firmament.mixins.start"))
- MixinPlugin.appliedMixins
- .map { it.removePrefix(MixinPlugin.mixinPackage) }
- .forEach {
- source.sendFeedback(Text.literal(" - ").withColor(0xD020F0)
- .append(Text.literal(it).withColor(0xF6BA20)))
- }
+ MixinPlugin.instances.forEach { plugin ->
+ source.sendFeedback(tr("firmament.mixins.start.package", "Mixins (base ${plugin.mixinPackage}):"))
+ plugin.appliedMixins
+ .map { it.removePrefix(plugin.mixinPackage) }
+ .forEach {
+ source.sendFeedback(Text.literal(" - ").withColor(0xD020F0)
+ .append(Text.literal(it).withColor(0xF6BA20)))
+ }
+ }
}
}
thenLiteral("repo") {
diff --git a/src/main/kotlin/events/CustomItemModelEvent.kt b/src/main/kotlin/events/CustomItemModelEvent.kt
index 21ee326..7b86980 100644
--- a/src/main/kotlin/events/CustomItemModelEvent.kt
+++ b/src/main/kotlin/events/CustomItemModelEvent.kt
@@ -1,10 +1,13 @@
package moe.nea.firmament.events
+import java.util.Objects
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
+import net.minecraft.component.DataComponentTypes
import net.minecraft.item.ItemStack
import net.minecraft.util.Identifier
import moe.nea.firmament.util.collections.WeakCache
+import moe.nea.firmament.util.collections.WeakCache.CacheFunction
import moe.nea.firmament.util.mc.IntrospectableItemModelManager
// TODO: assert an order on these events
@@ -14,7 +17,36 @@ data class CustomItemModelEvent(
var overrideModel: Identifier? = null,
) : FirmamentEvent() {
companion object : FirmamentEventBus<CustomItemModelEvent>() {
- val cache = WeakCache.memoize("ItemModelIdentifier", ::getModelIdentifier0)
+ val weakCache =
+ object : WeakCache<ItemStack, IntrospectableItemModelManager, Optional<Identifier>>("ItemModelIdentifier") {
+ override fun mkRef(
+ key: ItemStack,
+ extraData: IntrospectableItemModelManager
+ ): WeakCache<ItemStack, IntrospectableItemModelManager, Optional<Identifier>>.Ref {
+ return IRef(key, extraData)
+ }
+
+ inner class IRef(weakInstance: ItemStack, data: IntrospectableItemModelManager) :
+ Ref(weakInstance, data) {
+ override fun shouldBeEvicted(): Boolean = false
+ val isSimpleStack = weakInstance.componentChanges.isEmpty || (weakInstance.componentChanges.size() == 1 && weakInstance.get(
+ DataComponentTypes.CUSTOM_DATA)?.isEmpty == true)
+ val item = weakInstance.item
+ override fun hashCode(): Int {
+ if (isSimpleStack)
+ return Objects.hash(item, extraData)
+ return super.hashCode()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other is IRef && isSimpleStack) {
+ return other.isSimpleStack && item == other.item
+ }
+ return super.equals(other)
+ }
+ }
+ }
+ val cache = CacheFunction.WithExtraData(weakCache, ::getModelIdentifier0)
@JvmStatic
fun getModelIdentifier(itemStack: ItemStack?, itemModelManager: IntrospectableItemModelManager): Identifier? {
diff --git a/src/main/kotlin/events/EntityUpdateEvent.kt b/src/main/kotlin/events/EntityUpdateEvent.kt
index 27a90f9..fec2fa5 100644
--- a/src/main/kotlin/events/EntityUpdateEvent.kt
+++ b/src/main/kotlin/events/EntityUpdateEvent.kt
@@ -7,6 +7,8 @@ import net.minecraft.entity.LivingEntity
import net.minecraft.entity.data.DataTracker
import net.minecraft.item.ItemStack
import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.util.MC
/**
* This event is fired when some entity properties are updated.
@@ -15,7 +17,27 @@ import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket
* *after* the values have been applied to the entity.
*/
sealed class EntityUpdateEvent : FirmamentEvent() {
- companion object : FirmamentEventBus<EntityUpdateEvent>()
+ companion object : FirmamentEventBus<EntityUpdateEvent>() {
+ @Subscribe
+ fun onPlayerInventoryUpdate(event: PlayerInventoryUpdate) {
+ val p = MC.player ?: return
+ val updatedSlots = listOf(
+ EquipmentSlot.HEAD to 39,
+ EquipmentSlot.CHEST to 38,
+ EquipmentSlot.LEGS to 37,
+ EquipmentSlot.FEET to 36,
+ EquipmentSlot.OFFHAND to 40,
+ EquipmentSlot.MAINHAND to p.inventory.selectedSlot, // TODO: also equipment update when you swap your selected slot perhaps
+ ).mapNotNull { (slot, stackIndex) ->
+ val slotIndex = p.playerScreenHandler.getSlotIndex(p.inventory, stackIndex).asInt
+ event.getOrNull(slotIndex)?.let {
+ Pair.of(slot, it)
+ }
+ }
+ if (updatedSlots.isNotEmpty())
+ publish(EquipmentUpdate(p, updatedSlots))
+ }
+ }
abstract val entity: Entity
diff --git a/src/main/kotlin/events/PlayerInventoryUpdate.kt b/src/main/kotlin/events/PlayerInventoryUpdate.kt
index 6e8203a..88439a9 100644
--- a/src/main/kotlin/events/PlayerInventoryUpdate.kt
+++ b/src/main/kotlin/events/PlayerInventoryUpdate.kt
@@ -1,11 +1,22 @@
-
package moe.nea.firmament.events
import net.minecraft.item.ItemStack
sealed class PlayerInventoryUpdate : FirmamentEvent() {
- companion object : FirmamentEventBus<PlayerInventoryUpdate>()
- data class Single(val slot: Int, val stack: ItemStack) : PlayerInventoryUpdate()
- data class Multi(val contents: List<ItemStack>) : PlayerInventoryUpdate()
+ companion object : FirmamentEventBus<PlayerInventoryUpdate>()
+ data class Single(val slot: Int, val stack: ItemStack) : PlayerInventoryUpdate() {
+ override fun getOrNull(slot: Int): ItemStack? {
+ if (slot == this.slot) return stack
+ return null
+ }
+
+ }
+
+ data class Multi(val contents: List<ItemStack>) : PlayerInventoryUpdate() {
+ override fun getOrNull(slot: Int): ItemStack? {
+ return contents.getOrNull(slot)
+ }
+ }
+ abstract fun getOrNull(slot: Int): ItemStack?
}
diff --git a/src/main/kotlin/events/WorldKeyboardEvent.kt b/src/main/kotlin/events/WorldKeyboardEvent.kt
index e8566fd..1d6a758 100644
--- a/src/main/kotlin/events/WorldKeyboardEvent.kt
+++ b/src/main/kotlin/events/WorldKeyboardEvent.kt
@@ -1,18 +1,17 @@
-
-
package moe.nea.firmament.events
import net.minecraft.client.option.KeyBinding
import moe.nea.firmament.keybindings.IKeyBinding
data class WorldKeyboardEvent(val keyCode: Int, val scanCode: Int, val modifiers: Int) : FirmamentEvent.Cancellable() {
- companion object : FirmamentEventBus<WorldKeyboardEvent>()
+ companion object : FirmamentEventBus<WorldKeyboardEvent>()
- fun matches(keyBinding: KeyBinding): Boolean {
- return matches(IKeyBinding.minecraft(keyBinding))
- }
+ fun matches(keyBinding: KeyBinding): Boolean {
+ return matches(IKeyBinding.minecraft(keyBinding))
+ }
- fun matches(keyBinding: IKeyBinding): Boolean {
- return keyBinding.matches(keyCode, scanCode, modifiers)
- }
+ fun matches(keyBinding: IKeyBinding, atLeast: Boolean = false): Boolean {
+ return if (atLeast) keyBinding.matchesAtLeast(keyCode, scanCode, modifiers) else
+ keyBinding.matches(keyCode, scanCode, modifiers)
+ }
}
diff --git a/src/main/kotlin/events/WorldMouseMoveEvent.kt b/src/main/kotlin/events/WorldMouseMoveEvent.kt
new file mode 100644
index 0000000..7a17ba4
--- /dev/null
+++ b/src/main/kotlin/events/WorldMouseMoveEvent.kt
@@ -0,0 +1,5 @@
+package moe.nea.firmament.events
+
+data class WorldMouseMoveEvent(val deltaX: Double, val deltaY: Double) : FirmamentEvent.Cancellable() {
+ companion object : FirmamentEventBus<WorldMouseMoveEvent>()
+}
diff --git a/src/main/kotlin/features/FeatureManager.kt b/src/main/kotlin/features/FeatureManager.kt
index 1b39d4e..e0799c4 100644
--- a/src/main/kotlin/features/FeatureManager.kt
+++ b/src/main/kotlin/features/FeatureManager.kt
@@ -25,12 +25,17 @@ import moe.nea.firmament.features.inventory.PetFeatures
import moe.nea.firmament.features.inventory.PriceData
import moe.nea.firmament.features.inventory.SaveCursorPosition
import moe.nea.firmament.features.inventory.SlotLocking
+import moe.nea.firmament.features.inventory.WardrobeKeybinds
import moe.nea.firmament.features.inventory.buttons.InventoryButtons
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlay
+import moe.nea.firmament.features.items.EtherwarpOverlay
import moe.nea.firmament.features.mining.PickaxeAbility
import moe.nea.firmament.features.mining.PristineProfitTracker
+import moe.nea.firmament.features.misc.CustomCapes
+import moe.nea.firmament.features.misc.Hud
import moe.nea.firmament.features.world.FairySouls
import moe.nea.firmament.features.world.Waypoints
+import moe.nea.firmament.util.compatloader.ICompatMeta
import moe.nea.firmament.util.data.DataHolder
object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "features", ::Config) {
@@ -59,7 +64,6 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature
loadFeature(PowerUserTools)
loadFeature(Waypoints)
loadFeature(ChatLinks)
- loadFeature(InventoryButtons)
loadFeature(CompatibliltyFeatures)
loadFeature(AnniversaryFeatures)
loadFeature(QuickCommands)
@@ -67,6 +71,10 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature
loadFeature(SaveCursorPosition)
loadFeature(PriceData)
loadFeature(Fixes)
+ loadFeature(CustomCapes)
+ loadFeature(Hud)
+ loadFeature(EtherwarpOverlay)
+ loadFeature(WardrobeKeybinds)
loadFeature(DianaWaypoints)
loadFeature(ItemRarityCosmetics)
loadFeature(PickaxeAbility)
@@ -83,17 +91,18 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature
fun subscribeEvents() {
SubscriptionList.allLists.forEach { list ->
- runCatching {
- list.provideSubscriptions {
- it.owner.javaClass.classes.forEach {
- runCatching { it.getDeclaredField("INSTANCE").get(null) }
+ if (ICompatMeta.shouldLoad(list.javaClass.name))
+ runCatching {
+ list.provideSubscriptions {
+ it.owner.javaClass.classes.forEach {
+ runCatching { it.getDeclaredField("INSTANCE").get(null) }
+ }
+ subscribeSingleEvent(it)
}
- subscribeSingleEvent(it)
+ }.getOrElse {
+ // TODO: allow annotating source sets to specifically opt out of loading for mods, maybe automatically
+ Firmament.logger.info("Ignoring events from $list, likely due to a missing compat mod.", it)
}
- }.getOrElse {
- // TODO: allow annotating source sets to specifically opt out of loading for mods, maybe automatically
- Firmament.logger.info("Ignoring events from $list, likely due to a missing compat mod.", it)
- }
}
}
diff --git a/src/main/kotlin/features/chat/ChatLinks.kt b/src/main/kotlin/features/chat/ChatLinks.kt
index f85825b..1fb12e1 100644
--- a/src/main/kotlin/features/chat/ChatLinks.kt
+++ b/src/main/kotlin/features/chat/ChatLinks.kt
@@ -3,6 +3,7 @@ package moe.nea.firmament.features.chat
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsChannel
import io.ktor.utils.io.jvm.javaio.toInputStream
+import java.net.URI
import java.net.URL
import java.util.Collections
import java.util.concurrent.atomic.AtomicInteger
@@ -50,7 +51,7 @@ object ChatLinks : FirmamentFeature {
private fun isUrlAllowed(url: String) = isHostAllowed(url.removePrefix("https://").substringBefore("/"))
override val config get() = TConfig
- val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?( |$))".toRegex()
+ val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?(\\s|$))".toRegex()
val nextTexId = AtomicInteger(0)
data class Image(
@@ -78,7 +79,7 @@ object ChatLinks : FirmamentFeature {
val texId = Firmament.identifier("dynamic_image_preview${nextTexId.getAndIncrement()}")
MC.textureManager.registerTexture(
texId,
- NativeImageBackedTexture(image)
+ NativeImageBackedTexture({ texId.path }, image)
)
Image(texId, image.width, image.height)
} else
@@ -102,8 +103,8 @@ object ChatLinks : FirmamentFeature {
if (it.screen !is ChatScreen) return
val hoveredComponent =
MC.inGameHud.chatHud.getTextStyleAt(it.mouseX.toDouble(), it.mouseY.toDouble()) ?: return
- val hoverEvent = hoveredComponent.hoverEvent ?: return
- val value = hoverEvent.getValue(HoverEvent.Action.SHOW_TEXT) ?: return
+ val hoverEvent = hoveredComponent.hoverEvent as? HoverEvent.ShowText ?: return
+ val value = hoverEvent.value
val url = urlRegex.matchEntire(value.unformattedString)?.groupValues?.get(0) ?: return
if (!isImageUrl(url)) return
val imageFuture = imageCache[url] ?: return
@@ -138,19 +139,20 @@ object ChatLinks : FirmamentFeature {
var index = 0
while (index < text.length) {
val nextMatch = urlRegex.find(text, index)
- if (nextMatch == null) {
+ val url = nextMatch?.groupValues[0]
+ val uri = runCatching { url?.let(::URI) }.getOrNull()
+ if (nextMatch == null || url == null || uri == null) {
s.append(Text.literal(text.substring(index, text.length)))
break
}
val range = nextMatch.groups[0]!!.range
- val url = nextMatch.groupValues[0]
s.append(Text.literal(text.substring(index, range.first)))
s.append(
Text.literal(url).setStyle(
Style.EMPTY.withUnderline(true).withColor(
Formatting.AQUA
- ).withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal(url)))
- .withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, url))
+ ).withHoverEvent(HoverEvent.ShowText(Text.literal(url)))
+ .withClickEvent(ClickEvent.OpenUrl(uri))
)
)
if (isImageUrl(url))
diff --git a/src/main/kotlin/features/chat/CopyChat.kt b/src/main/kotlin/features/chat/CopyChat.kt
new file mode 100644
index 0000000..64f8734
--- /dev/null
+++ b/src/main/kotlin/features/chat/CopyChat.kt
@@ -0,0 +1,31 @@
+package moe.nea.firmament.features.chat
+
+import net.minecraft.text.OrderedText
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.ClientStartedEvent
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.reconstitute
+
+
+object CopyChat : FirmamentFeature {
+ override val identifier: String
+ get() = "copy-chat"
+
+ object TConfig : ManagedConfig(identifier, Category.CHAT) {
+ val copyChat by toggle("copy-chat") { false }
+ }
+
+ @Subscribe
+ fun onInit(event: ClientStartedEvent) {
+ }
+
+ override val config: ManagedConfig?
+ get() = TConfig
+
+ fun orderedTextToString(orderedText: OrderedText): String {
+ return orderedText.reconstitute().string
+ }
+
+
+}
diff --git a/src/main/kotlin/features/debug/AnimatedClothingScanner.kt b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt
index 11b47a9..4edccfb 100644
--- a/src/main/kotlin/features/debug/AnimatedClothingScanner.kt
+++ b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt
@@ -1,51 +1,193 @@
package moe.nea.firmament.features.debug
-import net.minecraft.component.DataComponentTypes
+import net.minecraft.command.argument.RegistryKeyArgumentType
+import net.minecraft.component.ComponentType
import net.minecraft.entity.Entity
+import net.minecraft.entity.decoration.ArmorStandEntity
+import net.minecraft.item.ItemStack
+import net.minecraft.nbt.NbtElement
+import net.minecraft.nbt.NbtOps
+import net.minecraft.registry.RegistryKeys
import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.get
+import moe.nea.firmament.commands.thenArgument
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.commands.thenLiteral
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.EntityUpdateEvent
+import moe.nea.firmament.events.WorldReadyEvent
+import moe.nea.firmament.util.ClipboardUtils
import moe.nea.firmament.util.MC
-import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.math.GChainReconciliation
+import moe.nea.firmament.util.math.GChainReconciliation.shortenCycle
+import moe.nea.firmament.util.mc.NbtPrism
import moe.nea.firmament.util.tr
object AnimatedClothingScanner {
- var observedEntity: Entity? = null
+ data class LensOfFashionTheft<T>(
+ val prism: NbtPrism,
+ val component: ComponentType<T>,
+ ) {
+ fun observe(itemStack: ItemStack): Collection<NbtElement> {
+ val x = itemStack.get(component) ?: return listOf()
+ val nbt = component.codecOrThrow.encodeStart(NbtOps.INSTANCE, x).orThrow
+ return prism.access(nbt)
+ }
+ }
+
+ var lens: LensOfFashionTheft<*>? = null
+ var subject: Entity? = null
+ var history: MutableList<String> = mutableListOf()
+ val metaHistory: MutableList<List<String>> = mutableListOf()
@OptIn(ExperimentalStdlibApi::class)
@Subscribe
fun onUpdate(event: EntityUpdateEvent) {
- if (event.entity != observedEntity) return
+ val s = subject ?: return
+ if (event.entity != s) return
+ val l = lens ?: return
if (event is EntityUpdateEvent.EquipmentUpdate) {
event.newEquipment.forEach {
- val id = it.second.skyBlockId?.neuItem
- val colour = it.second.get(DataComponentTypes.DYED_COLOR)
- ?.rgb?.toHexString(HexFormat.UpperCase)
- ?.let { " #$it" } ?: ""
- MC.sendChat(tr("firmament.fitstealer.update",
- "[FIT CHECK][${MC.currentTick}] ${it.first.asString()} => ${id}${colour}"))
+ val formatted = (l.observe(it.second)).joinToString()
+ history.add(formatted)
+ // TODO: add a slot filter
}
}
}
+ fun reduceHistory(reducer: (List<String>, List<String>) -> List<String>): List<String> {
+ return metaHistory.fold(history, reducer).shortenCycle()
+ }
+
@Subscribe
fun onSubCommand(event: CommandEvent.SubCommand) {
- event.subcommand("dev") {
+ event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
thenLiteral("stealthisfit") {
- thenExecute {
- observedEntity =
- if (observedEntity == null) MC.instance.targetedEntity else null
-
- MC.sendChat(
- observedEntity?.let {
- tr("firmament.fitstealer.targeted", "Observing the equipment of ${it.name}.")
- } ?: tr("firmament.fitstealer.targetlost", "No longer logging equipment."),
- )
+ thenLiteral("clear") {
+ thenExecute {
+ subject = null
+ metaHistory.clear()
+ history.clear()
+ MC.sendChat(tr("firmament.fitstealer.clear", "Cleared fit stealing history"))
+ }
+ }
+ thenLiteral("copy") {
+ thenExecute {
+ val history = reduceHistory { a, b -> a + b }
+ copyHistory(history)
+ MC.sendChat(tr("firmament.fitstealer.copied", "Copied the history"))
+ }
+ thenLiteral("deduplicated") {
+ thenExecute {
+ val history = reduceHistory { a, b ->
+ (a.toMutableSet() + b).toList()
+ }
+ copyHistory(history)
+ MC.sendChat(
+ tr(
+ "firmament.fitstealer.copied.deduplicated",
+ "Copied the deduplicated history"
+ )
+ )
+ }
+ }
+ thenLiteral("merged") {
+ thenExecute {
+ val history = reduceHistory(GChainReconciliation::reconcileCycles)
+ copyHistory(history)
+ MC.sendChat(tr("firmament.fitstealer.copied.merged", "Copied the merged history"))
+ }
+ }
+ }
+ thenLiteral("target") {
+ thenLiteral("self") {
+ thenExecute {
+ toggleObserve(MC.player!!)
+ }
+ }
+ thenLiteral("pet") {
+ thenExecute {
+ source.sendFeedback(
+ tr(
+ "firmament.fitstealer.stealingpet",
+ "Observing nearest marker armourstand"
+ )
+ )
+ val p = MC.player!!
+ val nearestPet = p.world.getEntitiesByClass(
+ ArmorStandEntity::class.java,
+ p.boundingBox.expand(10.0),
+ { it.isMarker })
+ .minBy { it.squaredDistanceTo(p) }
+ toggleObserve(nearestPet)
+ }
+ }
+ thenExecute {
+ val ent = MC.instance.targetedEntity
+ if (ent == null) {
+ source.sendFeedback(
+ tr(
+ "firmament.fitstealer.notargetundercursor",
+ "No entity under cursor"
+ )
+ )
+ } else {
+ toggleObserve(ent)
+ }
+ }
+ }
+ thenLiteral("path") {
+ thenArgument(
+ "component",
+ RegistryKeyArgumentType.registryKey(RegistryKeys.DATA_COMPONENT_TYPE)
+ ) { component ->
+ thenArgument("path", NbtPrism.Argument) { path ->
+ thenExecute {
+ lens = LensOfFashionTheft(
+ get(path),
+ MC.unsafeGetRegistryEntry(get(component))!!,
+ )
+ source.sendFeedback(
+ tr(
+ "firmament.fitstealer.lensset",
+ "Analyzing path ${get(path)} for component ${get(component).value}"
+ )
+ )
+ }
+ }
+ }
}
}
}
}
+
+ private fun copyHistory(toCopy: List<String>) {
+ ClipboardUtils.setTextContent(toCopy.joinToString("\n"))
+ }
+
+ @Subscribe
+ fun onWorldSwap(event: WorldReadyEvent) {
+ subject = null
+ if (history.isNotEmpty()) {
+ metaHistory.add(history)
+ history = mutableListOf()
+ }
+ }
+
+ private fun toggleObserve(entity: Entity?) {
+ subject = if (subject == null) entity else null
+ if (subject == null) {
+ metaHistory.add(history)
+ history = mutableListOf()
+ }
+ MC.sendChat(
+ subject?.let {
+ tr(
+ "firmament.fitstealer.targeted",
+ "Observing the equipment of ${it.name}."
+ )
+ } ?: tr("firmament.fitstealer.targetlost", "No longer logging equipment."),
+ )
+ }
}
diff --git a/src/main/kotlin/features/debug/DebugLogger.kt b/src/main/kotlin/features/debug/DebugLogger.kt
index 2c6b962..9115956 100644
--- a/src/main/kotlin/features/debug/DebugLogger.kt
+++ b/src/main/kotlin/features/debug/DebugLogger.kt
@@ -10,6 +10,7 @@ class DebugLogger(val tag: String) {
companion object {
val allInstances = InstanceList<DebugLogger>("DebugLogger")
}
+
object EnabledLogs : DataHolder<MutableSet<String>>(serializer(), "DebugLogs", ::mutableSetOf)
init {
@@ -17,6 +18,7 @@ class DebugLogger(val tag: String) {
}
fun isEnabled() = DeveloperFeatures.isEnabled && EnabledLogs.data.contains(tag)
+ fun log(text: String) = log { text }
fun log(text: () -> String) {
if (!isEnabled()) return
MC.sendChat(Text.literal(text()))
diff --git a/src/main/kotlin/features/debug/DeveloperFeatures.kt b/src/main/kotlin/features/debug/DeveloperFeatures.kt
index 8f0c25c..fd236f9 100644
--- a/src/main/kotlin/features/debug/DeveloperFeatures.kt
+++ b/src/main/kotlin/features/debug/DeveloperFeatures.kt
@@ -3,6 +3,10 @@ package moe.nea.firmament.features.debug
import java.io.File
import java.nio.file.Path
import java.util.concurrent.CompletableFuture
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.Type
+import org.objectweb.asm.tree.ClassNode
+import org.spongepowered.asm.mixin.Mixin
import kotlinx.serialization.json.encodeToStream
import kotlin.io.path.absolute
import kotlin.io.path.exists
@@ -10,14 +14,18 @@ import net.minecraft.client.MinecraftClient
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.DebugInstantiateEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.init.MixinPlugin
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.asm.AsmAnnotationUtil
import moe.nea.firmament.util.iterate
object DeveloperFeatures : FirmamentFeature {
+ val DEVELOPER_SUBCOMMAND: String = "dev"
override val identifier: String
get() = "developer"
override val config: TConfig
@@ -42,6 +50,42 @@ object DeveloperFeatures : FirmamentFeature {
}
@Subscribe
+ fun loadAllMixinClasses(event: DebugInstantiateEvent) {
+ val allMixinClasses = mutableSetOf<String>()
+ MixinPlugin.instances.forEach { plugin ->
+ val prefix = plugin.mixinPackage + "."
+ val classes = plugin.mixins.map { prefix + it }
+ allMixinClasses.addAll(classes)
+ for (cls in classes) {
+ val targets = javaClass.classLoader.getResourceAsStream("${cls.replace(".", "/")}.class").use {
+ val node = ClassNode()
+ ClassReader(it).accept(node, 0)
+ val mixins = mutableListOf<Mixin>()
+ (node.visibleAnnotations.orEmpty() + node.invisibleAnnotations.orEmpty()).forEach {
+ val annotationType = Type.getType(it.desc)
+ val mixinType = Type.getType(Mixin::class.java)
+ if (mixinType == annotationType) {
+ mixins.add(AsmAnnotationUtil.createProxy(Mixin::class.java, it))
+ }
+ }
+ mixins.flatMap { it.targets.toList() } + mixins.flatMap { it.value.map { it.java.name } }
+ }
+ for (target in targets)
+ try {
+ Firmament.logger.debug("Loading ${target} to force instantiate ${cls}")
+ Class.forName(target, true, javaClass.classLoader)
+ } catch (ex: Throwable) {
+ Firmament.logger.error("Could not load class ${target} that has been mixind by $cls", ex)
+ }
+ }
+ }
+ Firmament.logger.info("Forceloaded all Firmament mixins:")
+ val applied = MixinPlugin.instances.flatMap { it.appliedMixins }.toSet()
+ applied.forEach { Firmament.logger.info(" - ${it}") }
+ require(allMixinClasses == applied)
+ }
+
+ @Subscribe
fun dumpMissingTranslations(tickEvent: TickEvent) {
val toDump = missingTranslations ?: return
missingTranslations = null
@@ -60,9 +104,12 @@ object DeveloperFeatures : FirmamentFeature {
MC.sendChat(Text.translatable("firmament.dev.resourcerebuild.start"))
val startTime = TimeMark.now()
process.toHandle().onExit().thenApply {
- MC.sendChat(Text.stringifiedTranslatable(
- "firmament.dev.resourcerebuild.done",
- startTime.passedTime()))
+ MC.sendChat(
+ Text.stringifiedTranslatable(
+ "firmament.dev.resourcerebuild.done",
+ startTime.passedTime()
+ )
+ )
Unit
}
} else {
diff --git a/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt b/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt
new file mode 100644
index 0000000..f0250dc
--- /dev/null
+++ b/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt
@@ -0,0 +1,27 @@
+package moe.nea.firmament.features.debug
+
+import com.mojang.serialization.Codec
+import com.mojang.serialization.codecs.RecordCodecBuilder
+import java.util.Optional
+import net.minecraft.SharedConstants
+import moe.nea.firmament.Firmament
+
+data class ExportedTestConstantMeta(
+ val dataVersion: Int,
+ val modVersion: Optional<String>,
+) {
+ companion object {
+ val current = ExportedTestConstantMeta(
+ SharedConstants.getGameVersion().saveVersion.id,
+ Optional.of("Firmament ${Firmament.version.friendlyString}")
+ )
+
+ val CODEC: Codec<ExportedTestConstantMeta> = RecordCodecBuilder.create {
+ it.group(
+ Codec.INT.fieldOf("dataVersion").forGetter(ExportedTestConstantMeta::dataVersion),
+ Codec.STRING.optionalFieldOf("modVersion").forGetter(ExportedTestConstantMeta::modVersion),
+ ).apply(it, ::ExportedTestConstantMeta)
+ }
+ val SOURCE_CODEC = CODEC.fieldOf("source").codec()
+ }
+}
diff --git a/src/main/kotlin/features/debug/PowerUserTools.kt b/src/main/kotlin/features/debug/PowerUserTools.kt
index 8be5d5d..7c1df3f 100644
--- a/src/main/kotlin/features/debug/PowerUserTools.kt
+++ b/src/main/kotlin/features/debug/PowerUserTools.kt
@@ -1,31 +1,25 @@
package moe.nea.firmament.features.debug
-import com.mojang.serialization.Codec
-import com.mojang.serialization.DynamicOps
import com.mojang.serialization.JsonOps
-import com.mojang.serialization.codecs.RecordCodecBuilder
import kotlin.jvm.optionals.getOrNull
import net.minecraft.block.SkullBlock
import net.minecraft.block.entity.SkullBlockEntity
import net.minecraft.component.DataComponentTypes
import net.minecraft.component.type.ProfileComponent
import net.minecraft.entity.Entity
-import net.minecraft.entity.EntityType
import net.minecraft.entity.LivingEntity
import net.minecraft.item.ItemStack
import net.minecraft.item.Items
-import net.minecraft.nbt.NbtCompound
+import net.minecraft.nbt.NbtList
import net.minecraft.nbt.NbtOps
-import net.minecraft.nbt.NbtString
import net.minecraft.predicate.NbtPredicate
import net.minecraft.text.Text
import net.minecraft.text.TextCodecs
import net.minecraft.util.Identifier
+import net.minecraft.util.Nameable
import net.minecraft.util.hit.BlockHitResult
import net.minecraft.util.hit.EntityHitResult
import net.minecraft.util.hit.HitResult
-import net.minecraft.util.math.Position
-import net.minecraft.util.math.Vec3d
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.CustomItemModelEvent
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
@@ -43,8 +37,10 @@ import moe.nea.firmament.util.mc.IntrospectableItemModelManager
import moe.nea.firmament.util.mc.SNbtFormatter
import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.iterableArmorItems
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.tr
object PowerUserTools : FirmamentFeature {
override val identifier: String
@@ -59,6 +55,10 @@ object PowerUserTools : FirmamentFeature {
val copySkullTexture by keyBindingWithDefaultUnbound("copy-skull-texture")
val copyEntityData by keyBindingWithDefaultUnbound("entity-data")
val copyItemStack by keyBindingWithDefaultUnbound("copy-item-stack")
+ val copyTitle by keyBindingWithDefaultUnbound("copy-title")
+ val exportItemStackToRepo by keyBindingWithDefaultUnbound("export-item-stack")
+ val exportUIRecipes by keyBindingWithDefaultUnbound("export-recipe")
+ val exportNpcLocation by keyBindingWithDefaultUnbound("export-npc-location")
}
override val config
@@ -67,14 +67,13 @@ object PowerUserTools : FirmamentFeature {
var lastCopiedStack: Pair<ItemStack, Text>? = null
set(value) {
field = value
- if (value != null) lastCopiedStackViewTime = true
+ if (value != null) lastCopiedStackViewTime = 2
}
- var lastCopiedStackViewTime = false
+ var lastCopiedStackViewTime = 0
@Subscribe
fun resetLastCopiedStack(event: TickEvent) {
- if (!lastCopiedStackViewTime) lastCopiedStack = null
- lastCopiedStackViewTime = false
+ if (lastCopiedStackViewTime-- < 0) lastCopiedStack = null
}
@Subscribe
@@ -108,7 +107,7 @@ object PowerUserTools : FirmamentFeature {
MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.position", target.pos))
if (target is LivingEntity) {
MC.sendChat(Text.translatable("firmament.poweruser.entity.armor"))
- for (armorItem in target.armorItems) {
+ for ((slot, armorItem) in target.iterableArmorItems) {
MC.sendChat(Text.translatable("firmament.poweruser.entity.armor.item", debugFormat(armorItem)))
}
}
@@ -179,11 +178,23 @@ object PowerUserTools : FirmamentFeature {
Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skull-id", skullTexture.toString()))
println("Copied skull id: $skullTexture")
} else if (it.matches(TConfig.copyItemStack)) {
- ClipboardUtils.setTextContent(
- ItemStack.CODEC
- .encodeStart(MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE), item)
- .orThrow.toPrettyString())
+ val nbt = ItemStack.CODEC
+ .encodeStart(MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE), item)
+ .orThrow
+ ClipboardUtils.setTextContent(nbt.toPrettyString())
lastCopiedStack = Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.stack"))
+ } else if (it.matches(TConfig.copyTitle)) {
+ val allTitles = NbtList()
+ val inventoryNames =
+ it.screen.screenHandler.slots
+ .mapNotNullTo(mutableSetOf()) { it.inventory }
+ .filterIsInstance<Nameable>()
+ .map { it.name }
+ for (it in listOf(it.screen.title) + inventoryNames) {
+ allTitles.add(TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, it).result().getOrNull()!!)
+ }
+ ClipboardUtils.setTextContent(allTitles.toPrettyString())
+ MC.sendChat(tr("firmament.power-user.title.copied", "Copied screen and inventory titles"))
}
}
@@ -223,7 +234,7 @@ object PowerUserTools : FirmamentFeature {
lastCopiedStack = null
return
}
- lastCopiedStackViewTime = true
+ lastCopiedStackViewTime = 0
it.lines.add(text)
}
diff --git a/src/main/kotlin/features/debug/SoundVisualizer.kt b/src/main/kotlin/features/debug/SoundVisualizer.kt
new file mode 100644
index 0000000..f805e6b
--- /dev/null
+++ b/src/main/kotlin/features/debug/SoundVisualizer.kt
@@ -0,0 +1,65 @@
+package moe.nea.firmament.features.debug
+
+import net.minecraft.text.Text
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.SoundReceiveEvent
+import moe.nea.firmament.events.WorldReadyEvent
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.util.red
+import moe.nea.firmament.util.render.RenderInWorldContext
+
+object SoundVisualizer {
+
+ var showSounds = false
+
+ var sounds = mutableListOf<SoundReceiveEvent>()
+
+
+ @Subscribe
+ fun onSubCommand(event: CommandEvent.SubCommand) {
+ event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
+ thenLiteral("sounds") {
+ thenExecute {
+ showSounds = !showSounds
+ if (!showSounds) {
+ sounds.clear()
+ }
+ }
+ }
+ }
+ }
+
+ @Subscribe
+ fun onWorldSwap(event: WorldReadyEvent) {
+ sounds.clear()
+ }
+
+ @Subscribe
+ fun onRender(event: WorldRenderLastEvent) {
+ RenderInWorldContext.renderInWorld(event) {
+ sounds.forEach { event ->
+ withFacingThePlayer(event.position) {
+ text(
+ Text.literal(event.sound.value().id.toString()).also {
+ if (event.cancelled)
+ it.red()
+ },
+ verticalAlign = RenderInWorldContext.VerticalAlign.CENTER,
+ )
+ }
+ }
+ }
+ }
+
+ @Subscribe
+ fun onSoundReceive(event: SoundReceiveEvent) {
+ if (!showSounds) return
+ if (sounds.size > 1000) {
+ sounds.subList(0, 200).clear()
+ }
+ sounds.add(event)
+ }
+}
diff --git a/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt b/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt
new file mode 100644
index 0000000..4f9acd8
--- /dev/null
+++ b/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt
@@ -0,0 +1,255 @@
+package moe.nea.firmament.features.debug.itemeditor
+
+import kotlinx.coroutines.launch
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import net.minecraft.client.network.AbstractClientPlayerEntity
+import net.minecraft.entity.decoration.ArmorStandEntity
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HandledScreenKeyPressedEvent
+import moe.nea.firmament.events.WorldKeyboardEvent
+import moe.nea.firmament.features.debug.PowerUserTools
+import moe.nea.firmament.repo.ItemNameLookup
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.SHORT_NUMBER_FORMAT
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.async.waitForTextInput
+import moe.nea.firmament.util.ifDropLast
+import moe.nea.firmament.util.mc.ScreenUtil.getSlotByIndex
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.loreAccordingToNbt
+import moe.nea.firmament.util.mc.setSkullOwner
+import moe.nea.firmament.util.parseShortNumber
+import moe.nea.firmament.util.red
+import moe.nea.firmament.util.removeColorCodes
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.SkyBlockItems
+import moe.nea.firmament.util.tr
+import moe.nea.firmament.util.unformattedString
+import moe.nea.firmament.util.useMatch
+
+object ExportRecipe {
+
+
+ val xNames = "123"
+ val yNames = "ABC"
+
+ val slotIndices = (0..<9).map {
+ val x = it % 3
+ val y = it / 3
+
+ (yNames[y].toString() + xNames[x].toString()) to x + y * 9 + 10
+ }
+ val resultSlot = 25
+ val craftingTableSlut = resultSlot - 2
+
+ @Subscribe
+ fun exportNpcLocation(event: WorldKeyboardEvent) {
+ if (!event.matches(PowerUserTools.TConfig.exportNpcLocation)) {
+ return
+ }
+ val entity = MC.instance.targetedEntity
+ if (entity == null) {
+ MC.sendChat(tr("firmament.repo.export.npc.noentity", "Could not find entity to export"))
+ return
+ }
+ Firmament.coroutineScope.launch {
+ val guessName = entity.world.getEntitiesByClass(
+ ArmorStandEntity::class.java,
+ entity.boundingBox.expand(0.1),
+ { !it.name.string.contains("CLICK") })
+ .firstOrNull()?.customName?.string
+ ?: ""
+ val reply = waitForTextInput("$guessName (NPC)", "Export stub")
+ val id = generateName(reply)
+ ItemExporter.exportStub(id, "§9$reply") {
+ val playerEntity = entity as? AbstractClientPlayerEntity
+ val textureUrl = playerEntity?.skinTextures?.textureUrl
+ if (textureUrl != null)
+ it.setSkullOwner(playerEntity.uuid, textureUrl)
+ }
+ ItemExporter.modifyJson(id) {
+ val mutJson = it.toMutableMap()
+ mutJson["island"] = JsonPrimitive(SBData.skyblockLocation?.locrawMode ?: "unknown")
+ mutJson["x"] = JsonPrimitive(entity.blockX)
+ mutJson["y"] = JsonPrimitive(entity.blockY)
+ mutJson["z"] = JsonPrimitive(entity.blockZ)
+ JsonObject(mutJson)
+ }
+ }
+ }
+
+ @Subscribe
+ fun onRecipeKeyBind(event: HandledScreenKeyPressedEvent) {
+ if (!event.matches(PowerUserTools.TConfig.exportUIRecipes)) {
+ return
+ }
+ val title = event.screen.title.string
+ val sellSlot = event.screen.getSlotByIndex(49, false)?.stack
+ val craftingTableSlot = event.screen.getSlotByIndex(craftingTableSlut, false)
+ if (craftingTableSlot?.stack?.displayNameAccordingToNbt?.unformattedString == "Crafting Table") {
+ slotIndices.forEach { (_, index) ->
+ event.screen.getSlotByIndex(index, false)?.stack?.let(ItemExporter::ensureExported)
+ }
+ val inputs = slotIndices.associate { (name, index) ->
+ val id = event.screen.getSlotByIndex(index, false)?.stack?.takeIf { !it.isEmpty() }?.let {
+ "${it.skyBlockId?.neuItem}:${it.count}"
+ } ?: ""
+ name to JsonPrimitive(id)
+ }
+ val output = event.screen.getSlotByIndex(resultSlot, false)?.stack!!
+ val overrideOutputId = output.skyBlockId!!.neuItem
+ val count = output.count
+ val recipe = JsonObject(
+ inputs + mapOf(
+ "type" to JsonPrimitive("crafting"),
+ "count" to JsonPrimitive(count),
+ "overrideOutputId" to JsonPrimitive(overrideOutputId)
+ )
+ )
+ ItemExporter.appendRecipe(output.skyBlockId!!, recipe)
+ MC.sendChat(tr("firmament.repo.export.recipe", "Recipe for ${output.skyBlockId} exported."))
+ return
+ } else if (sellSlot?.displayNameAccordingToNbt?.string == "Sell Item" || (sellSlot?.loreAccordingToNbt
+ ?: listOf()).any { it.string == "Click to buyback!" }
+ ) {
+ val shopId = SkyblockId(title.uppercase().replace(" ", "_") + "_NPC")
+ if (!ItemExporter.isExported(shopId)) {
+ // TODO: export location + skin of last clicked npc
+ ItemExporter.exportStub(shopId, "§9$title (NPC)")
+ }
+ for (index in (9..9 * 5)) {
+ val item = event.screen.getSlotByIndex(index, false)?.stack ?: continue
+ val skyblockId = item.skyBlockId ?: continue
+ val costLines = item.loreAccordingToNbt
+ .map { it.string.trim() }
+ .dropWhile { !it.startsWith("Cost") }
+ .dropWhile { it == "Cost" }
+ .takeWhile { it != "Click to trade!" }
+ .takeWhile { it != "Stock" }
+ .filter { !it.isBlank() }
+ .map { it.removePrefix("Cost: ") }
+
+
+ val costs = costLines.mapNotNull { lineText ->
+ val line = findStackableItemByName(lineText)
+ if (line == null) {
+ MC.sendChat(
+ tr(
+ "firmament.repo.itemshop.fail",
+ "Could not parse cost item ${lineText} for ${item.displayNameAccordingToNbt}"
+ ).red()
+ )
+ }
+ line
+ }
+
+
+ ItemExporter.appendRecipe(
+ shopId, JsonObject(
+ mapOf(
+ "type" to JsonPrimitive("npc_shop"),
+ "cost" to JsonArray(costs.map { JsonPrimitive("${it.first.neuItem}:${it.second}") }),
+ "result" to JsonPrimitive("${skyblockId.neuItem}:${item.count}"),
+ )
+ )
+ )
+ }
+ MC.sendChat(tr("firmament.repo.export.itemshop", "Item Shop export for ${title} complete."))
+ } else {
+ MC.sendChat(tr("firmament.repo.export.recipe.fail", "No Recipe found"))
+ }
+ }
+
+ private val coinRegex = "(?<amount>$SHORT_NUMBER_FORMAT) Coins?".toPattern()
+ private val stackedItemRegex = "(?<name>.*) x(?<count>$SHORT_NUMBER_FORMAT)".toPattern()
+ private val reverseStackedItemRegex = "(?<count>$SHORT_NUMBER_FORMAT)x (?<name>.*)".toPattern()
+ private val essenceRegex = "(?<essence>.*) Essence x(?<count>$SHORT_NUMBER_FORMAT)".toPattern()
+ private val numberedItemRegex = "(?<count>$SHORT_NUMBER_FORMAT) (?<what>.*)".toPattern()
+
+ private val etherialRewardPattern = "\\+(?<amount>${SHORT_NUMBER_FORMAT})x? (?<what>.*)".toPattern()
+
+ fun findForName(name: String, fallbackToGenerated: Boolean = true): SkyblockId? {
+ var id = ItemNameLookup.guessItemByName(name, true)
+ if (id == null && fallbackToGenerated) {
+ id = generateName(name)
+ }
+ return id
+ }
+
+ fun skill(name: String): SkyblockId {
+ return SkyblockId("SKYBLOCK_SKILL_${name}")
+ }
+
+ fun generateName(name: String): SkyblockId {
+ return SkyblockId(name.uppercase().replace(" ", "_").replace("(", "").replace(")", ""))
+ }
+
+ fun findStackableItemByName(name: String, fallbackToGenerated: Boolean = false): Pair<SkyblockId, Double>? {
+ val properName = name.removeColorCodes().trim()
+ if (properName == "FREE" || properName == "This Chest is Free!") {
+ return Pair(SkyBlockItems.COINS, 0.0)
+ }
+ coinRegex.useMatch(properName) {
+ return Pair(SkyBlockItems.COINS, parseShortNumber(group("amount")))
+ }
+ etherialRewardPattern.useMatch(properName) {
+ val id = when (val id = group("what")) {
+ "Copper" -> SkyblockId("SKYBLOCK_COPPER")
+ "Bits" -> SkyblockId("SKYBLOCK_BIT")
+ "Garden Experience" -> SkyblockId("SKYBLOCK_SKILL_GARDEN")
+ "Farming XP" -> SkyblockId("SKYBLOCK_SKILL_FARMING")
+ "Gold Essence" -> SkyblockId("ESSENCE_GOLD")
+ "Gemstone Powder" -> SkyblockId("SKYBLOCK_POWDER_GEMSTONE")
+ "Mithril Powder" -> SkyblockId("SKYBLOCK_POWDER_MITHRIL")
+ "Pelts" -> SkyblockId("SKYBLOCK_PELT")
+ "Fine Flour" -> SkyblockId("FINE_FLOUR")
+ else -> {
+ id.ifDropLast(" Experience") {
+ skill(generateName(it).neuItem)
+ } ?: id.ifDropLast(" XP") {
+ skill(generateName(it).neuItem)
+ } ?: id.ifDropLast(" Powder") {
+ SkyblockId("SKYBLOCK_POWDER_${generateName(it).neuItem}")
+ } ?: id.ifDropLast(" Essence") {
+ SkyblockId("ESSENCE_${generateName(it).neuItem}")
+ } ?: generateName(id)
+ }
+ }
+ return Pair(id, parseShortNumber(group("amount")))
+ }
+ essenceRegex.useMatch(properName) {
+ return Pair(
+ SkyblockId("ESSENCE_${group("essence").uppercase()}"),
+ parseShortNumber(group("count"))
+ )
+ }
+ stackedItemRegex.useMatch(properName) {
+ val item = findForName(group("name"), fallbackToGenerated)
+ if (item != null) {
+ val count = parseShortNumber(group("count"))
+ return Pair(item, count)
+ }
+ }
+ reverseStackedItemRegex.useMatch(properName) {
+ val item = findForName(group("name"), fallbackToGenerated)
+ if (item != null) {
+ val count = parseShortNumber(group("count"))
+ return Pair(item, count)
+ }
+ }
+ numberedItemRegex.useMatch(properName) {
+ val item = findForName(group("what"), fallbackToGenerated)
+ if (item != null) {
+ val count = parseShortNumber(group("count"))
+ return Pair(item, count)
+ }
+ }
+
+ return findForName(properName, fallbackToGenerated)?.let { Pair(it, 1.0) }
+ }
+
+}
diff --git a/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt b/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt
new file mode 100644
index 0000000..d7d17aa
--- /dev/null
+++ b/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt
@@ -0,0 +1,184 @@
+package moe.nea.firmament.features.debug.itemeditor
+
+import com.mojang.brigadier.arguments.StringArgumentType
+import kotlinx.coroutines.launch
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlin.io.path.createParentDirectories
+import kotlin.io.path.exists
+import kotlin.io.path.notExists
+import kotlin.io.path.readText
+import kotlin.io.path.relativeTo
+import kotlin.io.path.writeText
+import net.minecraft.item.ItemStack
+import net.minecraft.item.Items
+import net.minecraft.nbt.NbtString
+import net.minecraft.text.Text
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.get
+import moe.nea.firmament.commands.suggestsList
+import moe.nea.firmament.commands.thenArgument
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.HandledScreenKeyPressedEvent
+import moe.nea.firmament.features.debug.DeveloperFeatures
+import moe.nea.firmament.features.debug.ExportedTestConstantMeta
+import moe.nea.firmament.features.debug.PowerUserTools
+import moe.nea.firmament.repo.RepoDownloadManager
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.util.LegacyTagParser
+import moe.nea.firmament.util.LegacyTagWriter.Companion.toLegacyString
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.focusedItemStack
+import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.loreAccordingToNbt
+import moe.nea.firmament.util.mc.toNbtList
+import moe.nea.firmament.util.setSkyBlockId
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.tr
+
+object ItemExporter {
+
+ fun exportItem(itemStack: ItemStack): Text {
+ val exporter = LegacyItemExporter.createExporter(itemStack)
+ val json = exporter.exportJson()
+ val jsonFormatted = Firmament.twoSpaceJson.encodeToString(json)
+ val fileName = json.jsonObject["internalname"]!!.jsonPrimitive.content
+ val itemFile = RepoDownloadManager.repoSavedLocation.resolve("items").resolve("${fileName}.json")
+ itemFile.createParentDirectories()
+ itemFile.writeText(jsonFormatted)
+ val overlayFile = RepoDownloadManager.repoSavedLocation.resolve("itemsOverlay")
+ .resolve(ExportedTestConstantMeta.current.dataVersion.toString())
+ .resolve("${fileName}.snbt")
+ overlayFile.createParentDirectories()
+ overlayFile.writeText(exporter.exportModernSnbt().toPrettyString())
+ return tr(
+ "firmament.repoexport.success",
+ "Exported item to ${itemFile.relativeTo(RepoDownloadManager.repoSavedLocation)}${
+ exporter.warnings.joinToString(
+ ""
+ ) { "\nWarning: $it" }
+ }"
+ )
+ }
+
+ fun pathFor(skyBlockId: SkyblockId) =
+ RepoManager.neuRepo.baseFolder.resolve("items/${skyBlockId.neuItem}.json")
+
+ fun isExported(skyblockId: SkyblockId) =
+ pathFor(skyblockId).exists()
+
+ fun ensureExported(itemStack: ItemStack) {
+ if (!isExported(itemStack.skyBlockId ?: return))
+ MC.sendChat(exportItem(itemStack))
+ }
+
+ fun modifyJson(skyblockId: SkyblockId, modify: (JsonObject) -> JsonObject) {
+ val oldJson = Firmament.json.decodeFromString<JsonObject>(pathFor(skyblockId).readText())
+ val newJson = modify(oldJson)
+ pathFor(skyblockId).writeText(Firmament.twoSpaceJson.encodeToString(JsonObject(newJson)))
+ }
+
+ fun appendRecipe(skyblockId: SkyblockId, recipe: JsonObject) {
+ modifyJson(skyblockId) { oldJson ->
+ val mutableJson = oldJson.toMutableMap()
+ val recipes = ((mutableJson["recipes"] as JsonArray?) ?: listOf()).toMutableList()
+ recipes.add(recipe)
+ mutableJson["recipes"] = JsonArray(recipes)
+ JsonObject(mutableJson)
+ }
+ }
+
+ @Subscribe
+ fun onCommand(event: CommandEvent.SubCommand) {
+ event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
+ thenLiteral("reexportlore") {
+ thenArgument("itemid", StringArgumentType.string()) { itemid ->
+ suggestsList { RepoManager.neuRepo.items.items.keys }
+ thenExecute {
+ val itemid = SkyblockId(get(itemid))
+ if (pathFor(itemid).notExists()) {
+ MC.sendChat(
+ tr(
+ "firmament.repo.export.relore.fail",
+ "Could not find json file to relore for ${itemid}"
+ )
+ )
+ }
+ fixLoreNbtFor(itemid)
+ MC.sendChat(
+ tr(
+ "firmament.repo.export.relore",
+ "Updated lore / display name for $itemid"
+ )
+ )
+ }
+ }
+ thenLiteral("all") {
+ thenExecute {
+ var i = 0
+ val chunkSize = 100
+ val items = RepoManager.neuRepo.items.items.keys
+ Firmament.coroutineScope.launch {
+ items.chunked(chunkSize).forEach { key ->
+ MC.sendChat(
+ tr(
+ "firmament.repo.export.relore.progress",
+ "Updated lore / display for ${i * chunkSize} / ${items.size}."
+ )
+ )
+ i++
+ key.forEach {
+ fixLoreNbtFor(SkyblockId(it))
+ }
+ }
+ MC.sendChat(tr("firmament.repo.export.relore.alldone", "All lores updated."))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fun fixLoreNbtFor(itemid: SkyblockId) {
+ modifyJson(itemid) {
+ val mutJson = it.toMutableMap()
+ val legacyTag = LegacyTagParser.parse(mutJson["nbttag"]!!.jsonPrimitive.content)
+ val display = legacyTag.getCompoundOrEmpty("display")
+ legacyTag.put("display", display)
+ display.putString("Name", mutJson["displayname"]!!.jsonPrimitive.content)
+ display.put(
+ "Lore",
+ (mutJson["lore"] as JsonArray).map { NbtString.of(it.jsonPrimitive.content) }
+ .toNbtList()
+ )
+ mutJson["nbttag"] = JsonPrimitive(legacyTag.toLegacyString())
+ JsonObject(mutJson)
+ }
+ }
+
+ @Subscribe
+ fun onKeyBind(event: HandledScreenKeyPressedEvent) {
+ if (event.matches(PowerUserTools.TConfig.exportItemStackToRepo)) {
+ val itemStack = event.screen.focusedItemStack ?: return
+ PowerUserTools.lastCopiedStack = (itemStack to exportItem(itemStack))
+ }
+ }
+
+ fun exportStub(skyblockId: SkyblockId, title: String, extra: (ItemStack) -> Unit = {}) {
+ exportItem(ItemStack(Items.PLAYER_HEAD).also {
+ it.displayNameAccordingToNbt = Text.literal(title)
+ it.loreAccordingToNbt = listOf(Text.literal(""))
+ it.setSkyBlockId(skyblockId)
+ extra(it) // LOL
+ })
+ MC.sendChat(tr("firmament.repo.export.stub", "Exported a stub item for $skyblockId"))
+ }
+}
diff --git a/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt b/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt
new file mode 100644
index 0000000..c0f48ca
--- /dev/null
+++ b/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt
@@ -0,0 +1,75 @@
+package moe.nea.firmament.features.debug.itemeditor
+
+import kotlinx.serialization.Serializable
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.item.ItemStack
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.util.Identifier
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
+import moe.nea.firmament.repo.ItemCache
+import moe.nea.firmament.util.MC
+
+/**
+ * Load data based on [prismarine.js' 1.8 item data](https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/1.8/items.json)
+ */
+object LegacyItemData {
+ @Serializable
+ data class ItemData(
+ val id: Int,
+ val name: String,
+ val displayName: String,
+ val stackSize: Int,
+ val variations: List<Variation> = listOf()
+ ) {
+ val properId = if (name.contains(":")) name else "minecraft:$name"
+
+ fun allVariants() =
+ variations.map { LegacyItemType(properId, it.metadata.toShort()) } + LegacyItemType(properId, 0)
+ }
+
+ @Serializable
+ data class Variation(
+ val metadata: Int, val displayName: String
+ )
+
+ data class LegacyItemType(
+ val name: String,
+ val metadata: Short
+ ) {
+ override fun toString(): String {
+ return "$name:$metadata"
+ }
+ }
+
+ @Serializable
+ data class EnchantmentData(
+ val id: Int,
+ val name: String,
+ val displayName: String,
+ )
+
+ inline fun <reified T : Any> getLegacyData(name: String) =
+ Firmament.tryDecodeJsonFromStream<T>(
+ LegacyItemData::class.java.getResourceAsStream("/legacy_data/$name.json")!!
+ ).getOrThrow()
+
+ val enchantmentData = getLegacyData<List<EnchantmentData>>("enchantments")
+ val enchantmentLut = enchantmentData.associateBy { Identifier.ofVanilla(it.name) }
+
+ val itemDat = getLegacyData<List<ItemData>>("items")
+ @OptIn(ExpensiveItemCacheApi::class) // This is fine, we get loaded in a thread.
+ val itemLut = itemDat.flatMap { item ->
+ item.allVariants().map { legacyItemType ->
+ val nbt = ItemCache.convert189ToModern(NbtCompound().apply {
+ putString("id", legacyItemType.name)
+ putByte("Count", 1)
+ putShort("Damage", legacyItemType.metadata)
+ })!!
+ val stack = ItemStack.fromNbt(MC.defaultRegistries, nbt).getOrNull()
+ ?: error("Could not transform ${legacyItemType}")
+ stack.item to legacyItemType
+ }
+ }.toMap()
+
+}
diff --git a/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt b/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt
new file mode 100644
index 0000000..3cd1ce8
--- /dev/null
+++ b/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt
@@ -0,0 +1,270 @@
+package moe.nea.firmament.features.debug.itemeditor
+
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.put
+import kotlin.concurrent.thread
+import net.minecraft.component.DataComponentTypes
+import net.minecraft.item.ItemStack
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.nbt.NbtElement
+import net.minecraft.nbt.NbtInt
+import net.minecraft.nbt.NbtOps
+import net.minecraft.nbt.NbtString
+import net.minecraft.text.Text
+import net.minecraft.util.Unit
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.ClientStartedEvent
+import moe.nea.firmament.features.debug.ExportedTestConstantMeta
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.util.HypixelPetInfo
+import moe.nea.firmament.util.LegacyTagWriter.Companion.toLegacyString
+import moe.nea.firmament.util.StringUtil.words
+import moe.nea.firmament.util.directLiteralStringContent
+import moe.nea.firmament.util.extraAttributes
+import moe.nea.firmament.util.getLegacyFormatString
+import moe.nea.firmament.util.json.toJsonArray
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.loreAccordingToNbt
+import moe.nea.firmament.util.mc.toNbtList
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.Rarity
+import moe.nea.firmament.util.transformEachRecursively
+import moe.nea.firmament.util.unformattedString
+
+class LegacyItemExporter private constructor(var itemStack: ItemStack) {
+ init {
+ require(!itemStack.isEmpty)
+ }
+ var lore = itemStack.loreAccordingToNbt
+ var name = itemStack.displayNameAccordingToNbt
+ val extraAttribs = itemStack.extraAttributes.copy()
+ val legacyNbt = NbtCompound()
+ val warnings = mutableListOf<String>()
+
+ // TODO: check if lore contains non 1.8.9 able hex codes and emit lore in overlay files if so
+
+ fun preprocess() {
+ // TODO: split up preprocess steps into preprocess actions that can be toggled in a ui
+ extraAttribs.remove("timestamp")
+ extraAttribs.remove("uuid")
+ extraAttribs.remove("modifier")
+ extraAttribs.getString("petInfo").ifPresent { petInfoJson ->
+ var petInfo = Firmament.json.decodeFromString<HypixelPetInfo>(petInfoJson)
+ petInfo = petInfo.copy(candyUsed = 0, heldItem = null, exp = 0.0, active = null, uuid = null)
+ extraAttribs.putString("petInfo", Firmament.tightJson.encodeToString(petInfo))
+ }
+ itemStack.skyBlockId?.let {
+ extraAttribs.putString("id", it.neuItem)
+ }
+ trimLore()
+ itemStack.loreAccordingToNbt = itemStack.item.defaultStack.loreAccordingToNbt
+ itemStack.remove(DataComponentTypes.CUSTOM_NAME)
+ }
+
+ fun trimLore() {
+ val rarityIdx = lore.indexOfLast {
+ val firstWordInLine = it.unformattedString.words().filter { it.length > 2 }.firstOrNull()
+ firstWordInLine?.let(Rarity::fromString) != null
+ }
+ if (rarityIdx >= 0) {
+ lore = lore.subList(0, rarityIdx + 1)
+ }
+
+ trimStats()
+
+ deleteLineUntilNextSpace { it.startsWith("Held Item: ") }
+ deleteLineUntilNextSpace { it.startsWith("Progress to Level ") }
+ deleteLineUntilNextSpace { it.startsWith("MAX LEVEL") }
+ deleteLineUntilNextSpace { it.startsWith("Click to view recipe!") }
+ collapseWhitespaces()
+
+ name = name.transformEachRecursively {
+ var string = it.directLiteralStringContent ?: return@transformEachRecursively it
+ string = string.replace("Lvl \\d+".toRegex(), "Lvl {LVL}")
+ Text.literal(string).setStyle(it.style)
+ }
+
+ if (lore.isEmpty())
+ lore = listOf(Text.empty())
+ }
+
+ private fun trimStats() {
+ val lore = this.lore.toMutableList()
+ for (index in lore.indices) {
+ val value = lore[index]
+ val statLine = SBItemStack.parseStatLine(value)
+ if (statLine == null) break
+ val v = value.copy()
+ require(value.directLiteralStringContent == "")
+ v.siblings.removeIf { it.directLiteralStringContent!!.contains("(") }
+ val last = v.siblings.last()
+ v.siblings[v.siblings.lastIndex] =
+ Text.literal(last.directLiteralStringContent!!.trimEnd())
+ .setStyle(last.style)
+ lore[index] = v
+ }
+ this.lore = lore
+ }
+
+ fun collapseWhitespaces() {
+ lore = (listOf(null as Text?) + lore).zipWithNext()
+ .filter { !it.first?.unformattedString.isNullOrBlank() || !it.second?.unformattedString.isNullOrBlank() }
+ .map { it.second!! }
+ }
+
+ fun deleteLineUntilNextSpace(search: (String) -> Boolean) {
+ val idx = lore.indexOfFirst { search(it.unformattedString) }
+ if (idx < 0) return
+ val l = lore.toMutableList()
+ val p = l.subList(idx, l.size)
+ val nextBlank = p.indexOfFirst { it.unformattedString.isEmpty() }
+ if (nextBlank < 0)
+ p.clear()
+ else
+ p.subList(0, nextBlank).clear()
+ lore = l
+ }
+
+ fun processNbt() {
+ // TODO: calculate hideflags
+ legacyNbt.put("HideFlags", NbtInt.of(254))
+ copyUnbreakable()
+ copyItemModel()
+ copyExtraAttributes()
+ copyLegacySkullNbt()
+ copyDisplay()
+ copyEnchantments()
+ copyEnchantGlint()
+ // TODO: copyDisplay
+ }
+
+ private fun copyItemModel() {
+ val itemModel = itemStack.get(DataComponentTypes.ITEM_MODEL) ?: return
+ legacyNbt.put("ItemModel", NbtString.of(itemModel.toString()))
+ }
+
+ private fun copyDisplay() {
+ legacyNbt.put("display", NbtCompound().apply {
+ put("Lore", lore.map { NbtString.of(it.getLegacyFormatString(trimmed = true)) }.toNbtList())
+ putString("Name", name.getLegacyFormatString(trimmed = true))
+ })
+ }
+
+ fun exportModernSnbt(): NbtElement {
+ val overlay = ItemStack.CODEC.encodeStart(NbtOps.INSTANCE, itemStack)
+ .orThrow
+ val overlayWithVersion =
+ ExportedTestConstantMeta.SOURCE_CODEC.encode(ExportedTestConstantMeta.current, NbtOps.INSTANCE, overlay)
+ .orThrow
+ return overlayWithVersion
+ }
+
+ fun prepare() {
+ preprocess()
+ processNbt()
+ itemStack.extraAttributes = extraAttribs
+ }
+
+ fun exportJson(): JsonElement {
+ return buildJsonObject {
+ val (itemId, damage) = legacyifyItemStack()
+ put("itemid", itemId)
+ put("displayname", name.getLegacyFormatString(trimmed = true))
+ put("nbttag", legacyNbt.toLegacyString())
+ put("damage", damage)
+ put("lore", lore.map { it.getLegacyFormatString(trimmed = true) }.toJsonArray())
+ val sbId = itemStack.skyBlockId
+ if (sbId == null)
+ warnings.add("Could not find skyblock id")
+ put("internalname", sbId?.neuItem)
+ put("clickcommand", "")
+ put("crafttext", "")
+ put("modver", "Firmament ${Firmament.version.friendlyString}")
+ put("infoType", "")
+ put("info", JsonArray(listOf()))
+ }
+
+ }
+
+ companion object {
+ fun createExporter(itemStack: ItemStack): LegacyItemExporter {
+ return LegacyItemExporter(itemStack.copy()).also { it.prepare() }
+ }
+
+ @Subscribe
+ fun load(event: ClientStartedEvent) {
+ thread(start = true, name = "ItemExporter Meta Load Thread") {
+ LegacyItemData.itemLut
+ }
+ }
+ }
+
+ fun copyEnchantGlint() {
+ if (itemStack.get(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE) == true) {
+ val ench = legacyNbt.getListOrEmpty("ench")
+ legacyNbt.put("ench", ench)
+ }
+ }
+
+ private fun copyUnbreakable() {
+ if (itemStack.get(DataComponentTypes.UNBREAKABLE) == Unit.INSTANCE) {
+ legacyNbt.putBoolean("Unbreakable", true)
+ }
+ }
+
+ fun copyEnchantments() {
+ val enchantments = itemStack.get(DataComponentTypes.ENCHANTMENTS)?.takeIf { !it.isEmpty } ?: return
+ val enchTag = legacyNbt.getListOrEmpty("ench")
+ legacyNbt.put("ench", enchTag)
+ enchantments.enchantmentEntries.forEach { entry ->
+ val id = entry.key.key.get().value
+ val legacyId = LegacyItemData.enchantmentLut[id]
+ if (legacyId == null) {
+ warnings.add("Could not find legacy enchantment id for ${id}")
+ return@forEach
+ }
+ enchTag.add(NbtCompound().apply {
+ putShort("lvl", entry.intValue.toShort())
+ putShort(
+ "id",
+ legacyId.id.toShort()
+ )
+ })
+ }
+ }
+
+ fun copyExtraAttributes() {
+ legacyNbt.put("ExtraAttributes", extraAttribs)
+ }
+
+ fun copyLegacySkullNbt() {
+ val profile = itemStack.get(DataComponentTypes.PROFILE) ?: return
+ legacyNbt.put("SkullOwner", NbtCompound().apply {
+ profile.id.ifPresent {
+ putString("Id", it.toString())
+ }
+ putBoolean("hypixelPopulated", true)
+ put("Properties", NbtCompound().apply {
+ profile.properties().forEach { prop, value ->
+ val list = getListOrEmpty(prop)
+ put(prop, list)
+ list.add(NbtCompound().apply {
+ value.signature?.let {
+ putString("Signature", it)
+ }
+ putString("Value", value.value)
+ putString("Name", value.name)
+ })
+ }
+ })
+ })
+ }
+
+ fun legacyifyItemStack(): LegacyItemData.LegacyItemType {
+ // TODO: add a default here
+ return LegacyItemData.itemLut[itemStack.item]!!
+ }
+}
diff --git a/src/main/kotlin/features/debug/itemeditor/PromptScreen.kt b/src/main/kotlin/features/debug/itemeditor/PromptScreen.kt
new file mode 100644
index 0000000..187b70b
--- /dev/null
+++ b/src/main/kotlin/features/debug/itemeditor/PromptScreen.kt
@@ -0,0 +1,15 @@
+package moe.nea.firmament.features.debug.itemeditor
+
+import io.github.notenoughupdates.moulconfig.gui.CloseEventListener
+import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper
+import io.github.notenoughupdates.moulconfig.gui.GuiContext
+import io.github.notenoughupdates.moulconfig.gui.component.CenterComponent
+import io.github.notenoughupdates.moulconfig.gui.component.ColumnComponent
+import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent
+import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
+import io.github.notenoughupdates.moulconfig.gui.component.TextFieldComponent
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
+import kotlin.reflect.KMutableProperty0
+import moe.nea.firmament.gui.FirmButtonComponent
+import moe.nea.firmament.util.MoulConfigUtils
+
diff --git a/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt
index 5151862..0cfaeba 100644
--- a/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt
+++ b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt
@@ -15,6 +15,7 @@ import moe.nea.firmament.events.WorldReadyEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.gui.hud.MoulConfigHud
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.ItemNameLookup
import moe.nea.firmament.repo.SBItemStack
import moe.nea.firmament.util.MC
@@ -202,7 +203,8 @@ object AnniversaryFeatures : FirmamentFeature {
SBItemStack(SkyblockId.NULL)
}
- @Bind
+ @OptIn(ExpensiveItemCacheApi::class)
+ @Bind
fun name(): String {
return when (backedBy) {
is Reward.Coins -> "Coins"
diff --git a/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt
index 1824225..cfc05cc 100644
--- a/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt
+++ b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt
@@ -222,7 +222,7 @@ object MinesweeperHelper {
fun onChat(event: ProcessChatEvent) {
if (CarnivalFeatures.TConfig.displayTutorials && event.unformattedString == startGameQuestion) {
MC.sendChat(Text.translatable("firmament.carnival.tutorial.minesweeper").styled {
- it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, "/firm minesweepertutorial"))
+ it.withClickEvent(ClickEvent.RunCommand("/firm minesweepertutorial"))
})
}
if (!CarnivalFeatures.TConfig.enableBombSolver) {
@@ -259,7 +259,7 @@ object MinesweeperHelper {
val boardPosition = BoardPosition.fromBlockPos(event.blockPos)
log.log { "Breaking block at ${event.blockPos} ($boardPosition)" }
gs.lastClickedPosition = boardPosition
- gs.lastDowsingMode = DowsingMode.fromItem(event.player.inventory.mainHandStack)
+ gs.lastDowsingMode = DowsingMode.fromItem(event.player.mainHandStack)
}
@Subscribe
diff --git a/src/main/kotlin/features/fixes/Fixes.kt b/src/main/kotlin/features/fixes/Fixes.kt
index 3dae233..d490cc4 100644
--- a/src/main/kotlin/features/fixes/Fixes.kt
+++ b/src/main/kotlin/features/fixes/Fixes.kt
@@ -11,6 +11,7 @@ import moe.nea.firmament.events.WorldKeyboardEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.tr
object Fixes : FirmamentFeature {
override val identifier: String
@@ -20,10 +21,14 @@ object Fixes : FirmamentFeature {
val fixUnsignedPlayerSkins by toggle("player-skins") { true }
var autoSprint by toggle("auto-sprint") { false }
val autoSprintKeyBinding by keyBindingWithDefaultUnbound("auto-sprint-keybinding")
+ val autoSprintUnderWater by toggle("auto-sprint-underwater") { true }
val autoSprintHud by position("auto-sprint-hud", 80, 10) { Point(0.0, 1.0) }
val peekChat by keyBindingWithDefaultUnbound("peek-chat")
val hidePotionEffects by toggle("hide-mob-effects") { false }
+ val hidePotionEffectsHud by toggle("hide-potion-effects-hud") { false }
val noHurtCam by toggle("disable-hurt-cam") { false }
+ val hideSlotHighlights by toggle("hide-slot-highlights") { false }
+ val hideRecipeBook by toggle("hide-recipe-book") { false }
}
override val config: ManagedConfig
@@ -33,8 +38,12 @@ object Fixes : FirmamentFeature {
keyBinding: KeyBinding,
cir: CallbackInfoReturnable<Boolean>
) {
- if (keyBinding === MinecraftClient.getInstance().options.sprintKey && TConfig.autoSprint && MC.player?.isSprinting != true)
- cir.returnValue = true
+ if (keyBinding !== MinecraftClient.getInstance().options.sprintKey) return
+ if (!TConfig.autoSprint) return
+ val player = MC.player ?: return
+ if (player.isSprinting) return
+ if (!TConfig.autoSprintUnderWater && player.isTouchingWater) return
+ cir.returnValue = true
}
@Subscribe
@@ -43,14 +52,18 @@ object Fixes : FirmamentFeature {
it.context.matrices.push()
TConfig.autoSprintHud.applyTransformations(it.context.matrices)
it.context.drawText(
- MC.font, Text.translatable(
- if (TConfig.autoSprint)
- "firmament.fixes.auto-sprint.on"
- else if (MC.player?.isSprinting == true)
- "firmament.fixes.auto-sprint.sprinting"
- else
- "firmament.fixes.auto-sprint.not-sprinting"
- ), 0, 0, -1, false
+ MC.font, (
+ if (MC.player?.isSprinting == true) {
+ Text.translatable("firmament.fixes.auto-sprint.sprinting")
+ } else if (TConfig.autoSprint) {
+ if (!TConfig.autoSprintUnderWater && MC.player?.isTouchingWater == true)
+ tr("firmament.fixes.auto-sprint.under-water", "In Water")
+ else
+ Text.translatable("firmament.fixes.auto-sprint.on")
+ } else {
+ Text.translatable("firmament.fixes.auto-sprint.not-sprinting")
+ }
+ ), 0, 0, -1, true
)
it.context.matrices.pop()
}
diff --git a/src/main/kotlin/features/garden/HideComposterNoises.kt b/src/main/kotlin/features/garden/HideComposterNoises.kt
new file mode 100644
index 0000000..69207a9
--- /dev/null
+++ b/src/main/kotlin/features/garden/HideComposterNoises.kt
@@ -0,0 +1,32 @@
+package moe.nea.firmament.features.garden
+
+import net.minecraft.entity.passive.WolfSoundVariants
+import net.minecraft.sound.SoundEvent
+import net.minecraft.sound.SoundEvents
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.SoundReceiveEvent
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.SkyBlockIsland
+
+object HideComposterNoises {
+ object TConfig : ManagedConfig("composter", Category.GARDEN) {
+ val hideComposterNoises by toggle("no-more-noises") { false }
+ }
+
+ val composterSoundEvents: List<SoundEvent> = listOf(
+ SoundEvents.BLOCK_PISTON_EXTEND,
+ SoundEvents.BLOCK_WATER_AMBIENT,
+ SoundEvents.ENTITY_CHICKEN_EGG,
+ SoundEvents.WOLF_SOUNDS[WolfSoundVariants.Type.CLASSIC]!!.growlSound().value(),
+ )
+
+ @Subscribe
+ fun onNoise(event: SoundReceiveEvent) {
+ if (!TConfig.hideComposterNoises) return
+ if (SBData.skyblockLocation == SkyBlockIsland.GARDEN) {
+ if (event.sound.value() in composterSoundEvents)
+ event.cancel()
+ }
+ }
+}
diff --git a/src/main/kotlin/features/inventory/CraftingOverlay.kt b/src/main/kotlin/features/inventory/CraftingOverlay.kt
index d2c79fd..f823086 100644
--- a/src/main/kotlin/features/inventory/CraftingOverlay.kt
+++ b/src/main/kotlin/features/inventory/CraftingOverlay.kt
@@ -8,6 +8,7 @@ import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.SlotRenderEvents
import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.SBItemStack
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.skyblockId
@@ -45,6 +46,7 @@ object CraftingOverlay : FirmamentFeature {
override val identifier: String
get() = "crafting-overlay"
+ @OptIn(ExpensiveItemCacheApi::class)
@Subscribe
fun onSlotRender(event: SlotRenderEvents.After) {
val slot = event.slot
diff --git a/src/main/kotlin/features/inventory/ItemHotkeys.kt b/src/main/kotlin/features/inventory/ItemHotkeys.kt
index 4aa8202..e826b31 100644
--- a/src/main/kotlin/features/inventory/ItemHotkeys.kt
+++ b/src/main/kotlin/features/inventory/ItemHotkeys.kt
@@ -3,12 +3,14 @@ package moe.nea.firmament.features.inventory
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.HypixelStaticData
import moe.nea.firmament.repo.ItemCache
import moe.nea.firmament.repo.ItemCache.asItemStack
import moe.nea.firmament.repo.ItemCache.isBroken
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.asBazaarStock
import moe.nea.firmament.util.focusedItemStack
import moe.nea.firmament.util.skyBlockId
import moe.nea.firmament.util.skyblock.SBItemUtil.getSearchName
@@ -18,6 +20,7 @@ object ItemHotkeys {
val openGlobalTradeInterface by keyBindingWithDefaultUnbound("global-trade-interface")
}
+ @OptIn(ExpensiveItemCacheApi::class)
@Subscribe
fun onHandledInventoryPress(event: HandledScreenKeyPressedEvent) {
if (!event.matches(TConfig.openGlobalTradeInterface)) {
@@ -26,7 +29,7 @@ object ItemHotkeys {
var item = event.screen.focusedItemStack ?: return
val skyblockId = item.skyBlockId ?: return
item = RepoManager.getNEUItem(skyblockId)?.asItemStack()?.takeIf { !it.isBroken } ?: item
- if (HypixelStaticData.hasBazaarStock(skyblockId)) {
+ if (HypixelStaticData.hasBazaarStock(skyblockId.asBazaarStock)) {
MC.sendCommand("bz ${item.getSearchName()}")
} else if (HypixelStaticData.hasAuctionHouseOffers(skyblockId)) {
MC.sendCommand("ahs ${item.getSearchName()}")
diff --git a/src/main/kotlin/features/inventory/PetFeatures.kt b/src/main/kotlin/features/inventory/PetFeatures.kt
index 5ca10f7..bb39fbc 100644
--- a/src/main/kotlin/features/inventory/PetFeatures.kt
+++ b/src/main/kotlin/features/inventory/PetFeatures.kt
@@ -1,14 +1,24 @@
package moe.nea.firmament.features.inventory
-import net.minecraft.util.Identifier
+import moe.nea.jarvis.api.Point
+import net.minecraft.item.ItemStack
+import net.minecraft.text.Text
+import net.minecraft.util.Formatting
+import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HudRenderEvent
import moe.nea.firmament.events.SlotRenderEvents
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.FirmFormatters.formatPercent
+import moe.nea.firmament.util.FirmFormatters.shortFormat
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.petData
import moe.nea.firmament.util.render.drawGuiTexture
+import moe.nea.firmament.util.skyblock.Rarity
+import moe.nea.firmament.util.titleCase
import moe.nea.firmament.util.useMatch
+import moe.nea.firmament.util.withColor
object PetFeatures : FirmamentFeature {
override val identifier: String
@@ -19,9 +29,12 @@ object PetFeatures : FirmamentFeature {
object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
val highlightEquippedPet by toggle("highlight-pet") { true }
+ var petOverlay by toggle("pet-overlay") { false }
+ val petOverlayHud by position("pet-overlay-hud", 80, 10) { Point(0.5, 1.0) }
}
val petMenuTitle = "Pets(?: \\([0-9]+/[0-9]+\\))?".toPattern()
+ var petItemStack: ItemStack? = null
@Subscribe
fun onSlotRender(event: SlotRenderEvents.Before) {
@@ -29,12 +42,44 @@ object PetFeatures : FirmamentFeature {
val stack = event.slot.stack
if (stack.petData?.active == true)
petMenuTitle.useMatch(MC.screenName ?: return) {
- event.context.drawGuiTexture(
- event.slot.x, event.slot.y, 0, 16, 16,
- Identifier.of("firmament:selected_pet_background")
- )
- }
+ petItemStack = stack
+ event.context.drawGuiTexture(
+ Firmament.identifier("selected_pet_background"),
+ event.slot.x, event.slot.y, 16, 16,
+ )
+ }
}
+ @Subscribe
+ fun onRenderHud(it: HudRenderEvent) {
+ if (!TConfig.petOverlay) return
+ val itemStack = petItemStack ?: return
+ val petData = petItemStack?.petData ?: return
+ val rarity = Rarity.fromNeuRepo(petData.tier)
+ val rarityCode = Rarity.colourMap[rarity] ?: Formatting.WHITE
+ val xp = petData.level
+ val petType = titleCase(petData.type)
+ val heldItem = petData.heldItem?.let { item -> "Held Item: ${titleCase(item)}" }
+
+ it.context.matrices.push()
+ TConfig.petOverlayHud.applyTransformations(it.context.matrices)
+
+ val lines = mutableListOf<Text>()
+ it.context.matrices.push()
+ it.context.matrices.translate(-0.5, -0.5, 0.0)
+ it.context.matrices.scale(2f, 2f, 1f)
+ it.context.drawItem(itemStack, 0, 0)
+ it.context.matrices.pop()
+ lines.add(Text.literal("[Lvl ${xp.currentLevel}] ").append(Text.literal(petType).withColor(rarityCode)))
+ if (heldItem != null) lines.add(Text.literal(heldItem))
+ if (xp.currentLevel != xp.maxLevel) lines.add(Text.literal("Required L${xp.currentLevel + 1}: ${shortFormat(xp.expInCurrentLevel.toDouble())}/${shortFormat(xp.expRequiredForNextLevel.toDouble())} (${formatPercent(xp.percentageToNextLevel.toDouble())})"))
+ lines.add(Text.literal("Required L100: ${shortFormat(xp.expTotal.toDouble())}/${shortFormat(xp.expRequiredForMaxLevel.toDouble())} (${formatPercent(xp.percentageToMaxLevel.toDouble())})"))
+
+ for ((index, line) in lines.withIndex()) {
+ it.context.drawText(MC.font, line.copy().withColor(Formatting.GRAY), 36, MC.font.fontHeight * index, -1, true)
+ }
+
+ it.context.matrices.pop()
+ }
}
diff --git a/src/main/kotlin/features/inventory/PriceData.kt b/src/main/kotlin/features/inventory/PriceData.kt
index 4477203..92bfc58 100644
--- a/src/main/kotlin/features/inventory/PriceData.kt
+++ b/src/main/kotlin/features/inventory/PriceData.kt
@@ -1,51 +1,120 @@
-
-
package moe.nea.firmament.features.inventory
+import org.lwjgl.glfw.GLFW
import net.minecraft.text.Text
+import net.minecraft.util.StringIdentifiable
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ItemTooltipEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.repo.HypixelStaticData
-import moe.nea.firmament.util.FirmFormatters
+import moe.nea.firmament.util.FirmFormatters.formatCommas
+import moe.nea.firmament.util.asBazaarStock
+import moe.nea.firmament.util.bold
+import moe.nea.firmament.util.darkGrey
+import moe.nea.firmament.util.gold
import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.tr
+import moe.nea.firmament.util.yellow
object PriceData : FirmamentFeature {
- override val identifier: String
- get() = "price-data"
-
- object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
- val tooltipEnabled by toggle("enable-always") { true }
- val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind")
- }
-
- override val config get() = TConfig
-
- @Subscribe
- fun onItemTooltip(it: ItemTooltipEvent) {
- if (!TConfig.tooltipEnabled && !TConfig.enableKeybinding.isPressed()) {
- return
- }
- val sbId = it.stack.skyBlockId
- val bazaarData = HypixelStaticData.bazaarData[sbId]
- val lowestBin = HypixelStaticData.lowestBin[sbId]
- if (bazaarData != null) {
- it.lines.add(Text.literal(""))
- it.lines.add(
- Text.stringifiedTranslatable("firmament.tooltip.bazaar.sell-order",
- FirmFormatters.formatCommas(bazaarData.quickStatus.sellPrice, 1))
- )
- it.lines.add(
- Text.stringifiedTranslatable("firmament.tooltip.bazaar.buy-order",
- FirmFormatters.formatCommas(bazaarData.quickStatus.buyPrice, 1))
- )
- } else if (lowestBin != null) {
- it.lines.add(Text.literal(""))
- it.lines.add(
- Text.stringifiedTranslatable("firmament.tooltip.ah.lowestbin",
- FirmFormatters.formatCommas(lowestBin, 1))
- )
- }
- }
+ override val identifier: String
+ get() = "price-data"
+
+ object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
+ val tooltipEnabled by toggle("enable-always") { true }
+ val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind")
+ val stackSizeKey by keyBinding("stack-size-keybind") { GLFW.GLFW_KEY_LEFT_SHIFT }
+ val avgLowestBin by choice(
+ "avg-lowest-bin-days",
+ ) {
+ AvgLowestBin.THREEDAYAVGLOWESTBIN
+ }
+ }
+
+ enum class AvgLowestBin : StringIdentifiable {
+ OFF,
+ ONEDAYAVGLOWESTBIN,
+ THREEDAYAVGLOWESTBIN,
+ SEVENDAYAVGLOWESTBIN;
+
+ override fun asString(): String {
+ return name
+ }
+ }
+
+ override val config get() = TConfig
+
+ fun formatPrice(label: Text, price: Double): Text {
+ return Text.literal("")
+ .yellow()
+ .bold()
+ .append(label)
+ .append(": ")
+ .append(
+ Text.literal(formatCommas(price, fractionalDigits = 1))
+ .append(if (price != 1.0) " coins" else " coin")
+ .gold()
+ .bold()
+ )
+ }
+
+ @Subscribe
+ fun onItemTooltip(it: ItemTooltipEvent) {
+ if (!TConfig.tooltipEnabled) return
+ if (TConfig.enableKeybinding.isBound && !TConfig.enableKeybinding.isPressed()) return
+ val sbId = it.stack.skyBlockId
+ val stackSize = it.stack.count
+ val isShowingStack = TConfig.stackSizeKey.isPressed()
+ val multiplier = if (isShowingStack) stackSize else 1
+ val multiplierText =
+ if (isShowingStack)
+ tr("firmament.tooltip.multiply", "Showing prices for x${stackSize}").darkGrey()
+ else
+ tr(
+ "firmament.tooltip.multiply.hint",
+ "[${TConfig.stackSizeKey.format()}] to show x${stackSize}"
+ ).darkGrey()
+ val bazaarData = HypixelStaticData.bazaarData[sbId?.asBazaarStock]
+ val lowestBin = HypixelStaticData.lowestBin[sbId]
+ val avgBinValue: Double? = when (TConfig.avgLowestBin) {
+ AvgLowestBin.ONEDAYAVGLOWESTBIN -> HypixelStaticData.avg1dlowestBin[sbId]
+ AvgLowestBin.THREEDAYAVGLOWESTBIN -> HypixelStaticData.avg3dlowestBin[sbId]
+ AvgLowestBin.SEVENDAYAVGLOWESTBIN -> HypixelStaticData.avg7dlowestBin[sbId]
+ AvgLowestBin.OFF -> null
+ }
+ if (bazaarData != null) {
+ it.lines.add(Text.literal(""))
+ it.lines.add(multiplierText)
+ it.lines.add(
+ formatPrice(
+ tr("firmament.tooltip.bazaar.sell-order", "Bazaar Sell Order"),
+ bazaarData.quickStatus.sellPrice * multiplier
+ )
+ )
+ it.lines.add(
+ formatPrice(
+ tr("firmament.tooltip.bazaar.buy-order", "Bazaar Buy Order"),
+ bazaarData.quickStatus.buyPrice * multiplier
+ )
+ )
+ } else if (lowestBin != null) {
+ it.lines.add(Text.literal(""))
+ it.lines.add(multiplierText)
+ it.lines.add(
+ formatPrice(
+ tr("firmament.tooltip.ah.lowestbin", "Lowest BIN"),
+ lowestBin * multiplier
+ )
+ )
+ if (avgBinValue != null) {
+ it.lines.add(
+ formatPrice(
+ tr("firmament.tooltip.ah.avg-lowestbin", "AVG Lowest BIN"),
+ avgBinValue * multiplier
+ )
+ )
+ }
+ }
+ }
}
diff --git a/src/main/kotlin/features/inventory/REIDependencyWarner.kt b/src/main/kotlin/features/inventory/REIDependencyWarner.kt
index 1e9b1b8..476759a 100644
--- a/src/main/kotlin/features/inventory/REIDependencyWarner.kt
+++ b/src/main/kotlin/features/inventory/REIDependencyWarner.kt
@@ -1,5 +1,6 @@
package moe.nea.firmament.features.inventory
+import java.net.URI
import net.fabricmc.loader.api.FabricLoader
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -38,7 +39,7 @@ object REIDependencyWarner {
.white()
.append(Text.literal("[").aqua())
.append(Text.translatable("firmament.download", modName)
- .styled { it.withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, modrinthLink(slug))) }
+ .styled { it.withClickEvent(ClickEvent.OpenUrl(URI (modrinthLink(slug)))) }
.yellow()
.also {
if (alreadyDownloaded)
@@ -51,6 +52,7 @@ object REIDependencyWarner {
@Subscribe
fun checkREIDependency(event: SkyblockServerUpdateEvent) {
if (!SBData.isOnSkyblock) return
+ if (!RepoManager.Config.warnForMissingItemListMod) return
if (hasREI) return
if (sentWarning) return
sentWarning = true
diff --git a/src/main/kotlin/features/inventory/SlotLocking.kt b/src/main/kotlin/features/inventory/SlotLocking.kt
index 0083c40..d3348a2 100644
--- a/src/main/kotlin/features/inventory/SlotLocking.kt
+++ b/src/main/kotlin/features/inventory/SlotLocking.kt
@@ -4,6 +4,7 @@ package moe.nea.firmament.features.inventory
import java.util.UUID
import org.lwjgl.glfw.GLFW
+import util.render.CustomRenderLayers
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
@@ -17,6 +18,9 @@ import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.int
import kotlinx.serialization.serializer
import net.minecraft.client.gui.screen.ingame.HandledScreen
+import net.minecraft.client.render.RenderLayer
+import net.minecraft.client.render.RenderLayers
+import net.minecraft.client.render.TexturedRenderLayers
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.screen.GenericContainerScreenHandler
import net.minecraft.screen.slot.Slot
@@ -45,7 +49,6 @@ import moe.nea.firmament.util.mc.ScreenUtil.getSlotByIndex
import moe.nea.firmament.util.mc.SlotUtils.swapWithHotBar
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
-import moe.nea.firmament.util.render.GuiRenderLayers
import moe.nea.firmament.util.render.drawLine
import moe.nea.firmament.util.skyblock.DungeonUtil
import moe.nea.firmament.util.skyblockUUID
@@ -445,7 +448,7 @@ object SlotLocking : FirmamentFeature {
val isUUIDLocked = (it.slot.stack?.skyblockUUID) in (lockedUUIDs ?: setOf())
if (isSlotLocked || isUUIDLocked) {
it.context.drawGuiTexture(
- GuiRenderLayers.GUI_TEXTURED_NO_DEPTH,
+ RenderLayer::getGuiTexturedOverlay,
when {
isSlotLocked ->
(Identifier.of("firmament:slot_locked"))
diff --git a/src/main/kotlin/features/inventory/TimerInLore.kt b/src/main/kotlin/features/inventory/TimerInLore.kt
index 309ea61..cc1df9a 100644
--- a/src/main/kotlin/features/inventory/TimerInLore.kt
+++ b/src/main/kotlin/features/inventory/TimerInLore.kt
@@ -16,12 +16,14 @@ import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.aqua
import moe.nea.firmament.util.grey
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.timestamp
import moe.nea.firmament.util.tr
import moe.nea.firmament.util.unformattedString
object TimerInLore {
object TConfig : ManagedConfig("lore-timers", Category.INVENTORY) {
val showTimers by toggle("show") { true }
+ val showCreationTimestamp by toggle("show-creation") { true }
val timerFormat by choice("format") { TimerFormat.SOCIALIST }
}
@@ -81,6 +83,9 @@ object TimerInLore {
CHOCOLATEFACTORY("Next Charge", "Available at"),
STONKSAUCTION("Auction ends in", "Ends at"),
LIZSTONKREDEMPTION("Resets in:", "Resets at"),
+ TIMEREMAININGS("Time Remaining:", "Ends at"),
+ COOLDOWN("Cooldown:", "Come back at"),
+ ONCOOLDOWN("On cooldown:", "Available at"),
EVENTENDING("Event ends in:", "Ends at");
}
@@ -88,6 +93,14 @@ object TimerInLore {
"(?i)(?:(?<years>[0-9]+) ?(y|years?) )?(?:(?<days>[0-9]+) ?(d|days?))? ?(?:(?<hours>[0-9]+) ?(h|hours?))? ?(?:(?<minutes>[0-9]+) ?(m|minutes?))? ?(?:(?<seconds>[0-9]+) ?(s|seconds?))?\\b".toRegex()
@Subscribe
+ fun creationInLore(event: ItemTooltipEvent) {
+ if (!TConfig.showCreationTimestamp) return
+ val timestamp = event.stack.timestamp ?: return
+ val formattedTimestamp = TConfig.timerFormat.formatter.format(ZonedDateTime.ofInstant(timestamp, ZoneId.systemDefault()))
+ event.lines.add(tr("firmament.lore.creationtimestamp", "Created at: $formattedTimestamp").grey())
+ }
+
+ @Subscribe
fun modifyLore(event: ItemTooltipEvent) {
if (!TConfig.showTimers) return
var lastTimer: ZonedDateTime? = null
@@ -108,9 +121,13 @@ object TimerInLore {
var baseLine = ZonedDateTime.now(SBData.hypixelTimeZone)
if (countdownType.isRelative) {
if (lastTimer == null) {
- event.lines.add(i + 1,
- tr("firmament.loretimer.missingrelative",
- "Found a relative countdown with no baseline (Firmament)").grey())
+ event.lines.add(
+ i + 1,
+ tr(
+ "firmament.loretimer.missingrelative",
+ "Found a relative countdown with no baseline (Firmament)"
+ ).grey()
+ )
continue
}
baseLine = lastTimer
@@ -120,10 +137,11 @@ object TimerInLore {
lastTimer = timer
val localTimer = timer.withZoneSameInstant(ZoneId.systemDefault())
// TODO: install approximate time stabilization algorithm
- event.lines.add(i + 1,
- Text.literal("${countdownType.label}: ")
- .grey()
- .append(Text.literal(TConfig.timerFormat.formatter.format(localTimer)).aqua())
+ event.lines.add(
+ i + 1,
+ Text.literal("${countdownType.label}: ")
+ .grey()
+ .append(Text.literal(TConfig.timerFormat.formatter.format(localTimer)).aqua())
)
}
}
diff --git a/src/main/kotlin/features/inventory/WardrobeKeybinds.kt b/src/main/kotlin/features/inventory/WardrobeKeybinds.kt
new file mode 100644
index 0000000..d797600
--- /dev/null
+++ b/src/main/kotlin/features/inventory/WardrobeKeybinds.kt
@@ -0,0 +1,56 @@
+package moe.nea.firmament.features.inventory
+
+import org.lwjgl.glfw.GLFW
+import net.minecraft.item.Items
+import net.minecraft.screen.slot.SlotActionType
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HandledScreenKeyPressedEvent
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.mc.SlotUtils.clickLeftMouseButton
+import moe.nea.firmament.util.mc.SlotUtils.clickMiddleMouseButton
+
+object WardrobeKeybinds : FirmamentFeature {
+ override val identifier: String
+ get() = "wardrobe-keybinds"
+
+ object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
+ val wardrobeKeybinds by toggle("wardrobe-keybinds") { false }
+ val slotKeybinds = (1..9).map {
+ keyBinding("slot-$it") { GLFW.GLFW_KEY_0 + it }
+ }
+ }
+
+ override val config: ManagedConfig?
+ get() = TConfig
+
+ val slotKeybindsWithSlot = TConfig.slotKeybinds.withIndex().map { (index, keybinding) ->
+ index + 36 to keybinding
+ }
+
+ @Subscribe
+ fun switchSlot(event: HandledScreenKeyPressedEvent) {
+ if (MC.player == null || MC.world == null || MC.interactionManager == null) return
+
+ val regex = Regex("Wardrobe \\([12]/2\\)")
+ if (!regex.matches(event.screen.title.string)) return
+ if (!TConfig.wardrobeKeybinds) return
+
+ val slot =
+ slotKeybindsWithSlot
+ .find { event.matches(it.second.get()) }
+ ?.first ?: return
+
+ event.cancel()
+
+ val handler = event.screen.screenHandler
+ val invSlot = handler.getSlot(slot)
+
+ val itemStack = invSlot.stack
+ if (itemStack.item != Items.PINK_DYE && itemStack.item != Items.LIME_DYE) return
+
+ invSlot.clickLeftMouseButton(handler)
+ }
+
+}
diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButton.kt b/src/main/kotlin/features/inventory/buttons/InventoryButton.kt
index a46bd76..955ae88 100644
--- a/src/main/kotlin/features/inventory/buttons/InventoryButton.kt
+++ b/src/main/kotlin/features/inventory/buttons/InventoryButton.kt
@@ -1,5 +1,3 @@
-
-
package moe.nea.firmament.features.inventory.buttons
import com.mojang.brigadier.StringReader
@@ -13,74 +11,93 @@ import net.minecraft.command.argument.ItemStackArgumentType
import net.minecraft.item.ItemStack
import net.minecraft.resource.featuretoggle.FeatureFlags
import net.minecraft.util.Identifier
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.ItemCache.asItemStack
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.collections.memoize
+import moe.nea.firmament.util.mc.arbitraryUUID
+import moe.nea.firmament.util.mc.createSkullItem
import moe.nea.firmament.util.render.drawGuiTexture
@Serializable
data class InventoryButton(
- var x: Int,
- var y: Int,
- var anchorRight: Boolean,
- var anchorBottom: Boolean,
- var icon: String? = "",
- var command: String? = "",
+ var x: Int,
+ var y: Int,
+ var anchorRight: Boolean,
+ var anchorBottom: Boolean,
+ var icon: String? = "",
+ var command: String? = "",
) {
- companion object {
- val itemStackParser by lazy {
- ItemStackArgumentType.itemStack(CommandRegistryAccess.of(MC.defaultRegistries,
- FeatureFlags.VANILLA_FEATURES))
- }
- val dimensions = Dimension(18, 18)
- val getItemForName = ::getItemForName0.memoize(1024)
- fun getItemForName0(icon: String): ItemStack {
- val repoItem = RepoManager.getNEUItem(SkyblockId(icon))
- var itemStack = repoItem.asItemStack(idHint = SkyblockId(icon))
- if (repoItem == null) {
- val giveSyntaxItem = if (icon.startsWith("/give") || icon.startsWith("give"))
- icon.split(" ", limit = 3).getOrNull(2) ?: icon
- else icon
- val componentItem =
- runCatching {
- itemStackParser.parse(StringReader(giveSyntaxItem)).createStack(1, false)
- }.getOrNull()
- if (componentItem != null)
- itemStack = componentItem
- }
- return itemStack
- }
- }
+ companion object {
+ val itemStackParser by lazy {
+ ItemStackArgumentType.itemStack(
+ CommandRegistryAccess.of(
+ MC.defaultRegistries,
+ FeatureFlags.VANILLA_FEATURES
+ )
+ )
+ }
+ val dimensions = Dimension(18, 18)
+ val getItemForName = ::getItemForName0.memoize(1024)
+ @OptIn(ExpensiveItemCacheApi::class)
+ fun getItemForName0(icon: String): ItemStack {
+ val repoItem = RepoManager.getNEUItem(SkyblockId(icon))
+ var itemStack = repoItem.asItemStack(idHint = SkyblockId(icon))
+ if (repoItem == null) {
+ when {
+ icon.startsWith("skull:") -> {
+ itemStack = createSkullItem(
+ arbitraryUUID,
+ "https://textures.minecraft.net/texture/${icon.substring("skull:".length)}"
+ )
+ }
+
+ else -> {
+ val giveSyntaxItem = if (icon.startsWith("/give") || icon.startsWith("give"))
+ icon.split(" ", limit = 3).getOrNull(2) ?: icon
+ else icon
+ val componentItem =
+ runCatching {
+ itemStackParser.parse(StringReader(giveSyntaxItem)).createStack(1, false)
+ }.getOrNull()
+ if (componentItem != null)
+ itemStack = componentItem
+ }
+ }
+ }
+ return itemStack
+ }
+ }
- fun render(context: DrawContext) {
- context.drawGuiTexture(
- 0,
- 0,
- 0,
- dimensions.width,
- dimensions.height,
- Identifier.of("firmament:inventory_button_background")
- )
- context.drawItem(getItem(), 1, 1)
- }
+ fun render(context: DrawContext) {
+ context.drawGuiTexture(
+ 0,
+ 0,
+ 0,
+ dimensions.width,
+ dimensions.height,
+ Identifier.of("firmament:inventory_button_background")
+ )
+ context.drawItem(getItem(), 1, 1)
+ }
- fun isValid() = !icon.isNullOrBlank() && !command.isNullOrBlank()
+ fun isValid() = !icon.isNullOrBlank() && !command.isNullOrBlank()
- fun getPosition(guiRect: Rectangle): Point {
- return Point(
- (if (anchorRight) guiRect.maxX else guiRect.minX) + x,
- (if (anchorBottom) guiRect.maxY else guiRect.minY) + y,
- )
- }
+ fun getPosition(guiRect: Rectangle): Point {
+ return Point(
+ (if (anchorRight) guiRect.maxX else guiRect.minX) + x,
+ (if (anchorBottom) guiRect.maxY else guiRect.minY) + y,
+ )
+ }
- fun getBounds(guiRect: Rectangle): Rectangle {
- return Rectangle(getPosition(guiRect), dimensions)
- }
+ fun getBounds(guiRect: Rectangle): Rectangle {
+ return Rectangle(getPosition(guiRect), dimensions)
+ }
- fun getItem(): ItemStack {
- return getItemForName(icon ?: "")
- }
+ fun getItem(): ItemStack {
+ return getItemForName(icon ?: "")
+ }
}
diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt
index ee3ae8b..eecbd17 100644
--- a/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt
+++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt
@@ -1,13 +1,17 @@
package moe.nea.firmament.features.inventory.buttons
import io.github.notenoughupdates.moulconfig.common.IItemStack
+import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent
import io.github.notenoughupdates.moulconfig.platform.ModernItemStack
+import io.github.notenoughupdates.moulconfig.platform.ModernRenderContext
import io.github.notenoughupdates.moulconfig.xml.Bind
import me.shedaniel.math.Point
import me.shedaniel.math.Rectangle
import org.lwjgl.glfw.GLFW
+import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.widget.ButtonWidget
+import net.minecraft.client.gui.widget.TextWidget
import net.minecraft.client.util.InputUtil
import net.minecraft.text.Text
import net.minecraft.util.math.MathHelper
@@ -55,9 +59,47 @@ class InventoryButtonEditor(
super.close()
}
+ override fun resize(client: MinecraftClient, width: Int, height: Int) {
+ lastGuiRect.move(
+ MC.window.scaledWidth / 2 - lastGuiRect.width / 2,
+ MC.window.scaledHeight / 2 - lastGuiRect.height / 2
+ )
+ super.resize(client, width, height)
+ }
+
override fun init() {
super.init()
addDrawableChild(
+ TextWidget(
+ lastGuiRect.minX,
+ 25,
+ lastGuiRect.width,
+ 9,
+ Text.translatable("firmament.inventory-buttons.delete"),
+ MC.font
+ ).alignCenter()
+ )
+ addDrawableChild(
+ TextWidget(
+ lastGuiRect.minX,
+ 40,
+ lastGuiRect.width,
+ 9,
+ Text.translatable("firmament.inventory-buttons.info"),
+ MC.font
+ ).alignCenter()
+ )
+ addDrawableChild(
+ ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.reset")) {
+ val newButtons = InventoryButtonTemplates.loadTemplate("TkVVQlVUVE9OUy9bXQ==")
+ if (newButtons != null)
+ buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) })
+ }
+ .position(lastGuiRect.minX + 10, lastGuiRect.minY + 10)
+ .width(lastGuiRect.width - 20)
+ .build()
+ )
+ addDrawableChild(
ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.load-preset")) {
val t = ClipboardUtils.getTextContents()
val newButtons = InventoryButtonTemplates.loadTemplate(t)
@@ -76,6 +118,30 @@ class InventoryButtonEditor(
.width(lastGuiRect.width - 20)
.build()
)
+ addDrawableChild(
+ ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.simple-preset")) {
+ // Preset from NEU
+ // Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/resources/assets/notenoughupdates/invbuttons/presets.json#L900-L1348
+ val newButtons = InventoryButtonTemplates.loadTemplate("TkVVQlVUVE9OUy9bIntcblx0XCJ4XCI6IDE2MCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcImJvbmVcIixcblx0XCJjb21tYW5kXCI6IFwicGV0c1wiXG59Iiwie1xuXHRcInhcIjogMTQwLFxuXHRcInlcIjogLTIwLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwiYXJtb3Jfc3RhbmRcIixcblx0XCJjb21tYW5kXCI6IFwid2FyZHJvYmVcIlxufSIsIntcblx0XCJ4XCI6IDEyMCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcImVuZGVyX2NoZXN0XCIsXG5cdFwiY29tbWFuZFwiOiBcInN0b3JhZ2VcIlxufSIsIntcblx0XCJ4XCI6IDEwMCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcInNrdWxsOmQ3Y2M2Njg3NDIzZDA1NzBkNTU2YWM1M2UwNjc2Y2I1NjNiYmRkOTcxN2NkODI2OWJkZWJlZDZmNmQ0ZTdiZjhcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBpc2xhbmRcIlxufSIsIntcblx0XCJ4XCI6IDgwLFxuXHRcInlcIjogLTIwLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwic2t1bGw6MzVmNGI0MGNlZjllMDE3Y2Q0MTEyZDI2YjYyNTU3ZjhjMWQ1YjE4OWRhMmU5OTUzNDIyMmJjOGNlYzdkOTE5NlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGh1YlwiXG59Il0=")
+ if (newButtons != null)
+ buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) })
+ }
+ .position(lastGuiRect.minX + 10, lastGuiRect.minY + 85)
+ .width(lastGuiRect.width - 20)
+ .build()
+ )
+ addDrawableChild(
+ ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.all-warps-preset")) {
+ // Preset from NEU
+ // Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/resources/assets/notenoughupdates/invbuttons/presets.json#L1817-L2276
+ val newButtons = InventoryButtonTemplates.loadTemplate("TkVVQlVUVE9OUy9bIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtODQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6YzljODg4MWU0MjkxNWE5ZDI5YmI2MWExNmZiMjZkMDU5OTEzMjA0ZDI2NWRmNWI0MzliM2Q3OTJhY2Q1NlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGhvbWVcIlxufSIsIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtNjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6ZDdjYzY2ODc0MjNkMDU3MGQ1NTZhYzUzZTA2NzZjYjU2M2JiZGQ5NzE3Y2Q4MjY5YmRlYmVkNmY2ZDRlN2JmOFwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGh1YlwiXG59Iiwie1xuXHRcInhcIjogMixcblx0XCJ5XCI6IC00NCxcblx0XCJhbmNob3JSaWdodFwiOiB0cnVlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo5YjU2ODk1Yjk2NTk4OTZhZDY0N2Y1ODU5OTIzOGFmNTMyZDQ2ZGI5YzFiMDM4OWI4YmJlYjcwOTk5ZGFiMzNkXCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZHVuZ2Vvbl9odWJcIlxufSIsIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtMjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6Nzg0MGI4N2Q1MjI3MWQyYTc1NWRlZGM4Mjg3N2UwZWQzZGY2N2RjYzQyZWE0NzllYzE0NjE3NmIwMjc3OWE1XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZW5kXCJcbn0iLCJ7XG5cdFwieFwiOiAxMDksXG5cdFwieVwiOiAtMTksXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IGZhbHNlLFxuXHRcImljb25cIjogXCJza3VsbDo4NmYwNmVhYTMwMDRhZWVkMDliM2Q1YjQ1ZDk3NmRlNTg0ZTY5MWMwZTljYWRlMTMzNjM1ZGU5M2QyM2I5ZWRiXCIsXG5cdFwiY29tbWFuZFwiOiBcImhvdG1cIlxufSIsIntcblx0XCJ4XCI6IDEzMCxcblx0XCJ5XCI6IC0xOSxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkVOREVSX0NIRVNUXCIsXG5cdFwiY29tbWFuZFwiOiBcInN0b3JhZ2VcIlxufSIsIntcblx0XCJ4XCI6IDE1MSxcblx0XCJ5XCI6IC0xOSxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkJPTkVcIixcblx0XCJjb21tYW5kXCI6IFwicGV0c1wiXG59Iiwie1xuXHRcInhcIjogLTE5LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkdPTERfQkxPQ0tcIixcblx0XCJjb21tYW5kXCI6IFwiYWhcIlxufSIsIntcblx0XCJ4XCI6IC0xOSxcblx0XCJ5XCI6IDIyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwiR09MRF9CQVJESU5HXCIsXG5cdFwiY29tbWFuZFwiOiBcImJ6XCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtODQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjQzOGNmM2Y4ZTU0YWZjM2IzZjkxZDIwYTQ5ZjMyNGRjYTE0ODYwMDdmZTU0NTM5OTA1NTUyNGMxNzk0MWY0ZGNcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBtdXNldW1cIlxufSIsIntcblx0XCJ4XCI6IC0xOSxcblx0XCJ5XCI6IC02NCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6ZjQ4ODBkMmMxZTdiODZlODc1MjJlMjA4ODI2NTZmNDViYWZkNDJmOTQ5MzJiMmM1ZTBkNmVjYWE0OTBjYjRjXCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZ2FyZGVuXCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtNDQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjRkM2E2YmQ5OGFjMTgzM2M2NjRjNDkwOWZmOGQyZGM2MmNlODg3YmRjZjNjYzViMzg0ODY1MWFlNWFmNmJcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBiYXJuXCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtMjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjUxNTM5ZGRkZjllZDI1NWVjZTYzNDgxOTNjZDc1MDEyYzgyYzkzYWVjMzgxZjA1NTcyY2VjZjczNzk3MTFiM2JcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBkZXNlcnRcIlxufSIsIntcblx0XCJ4XCI6IDQsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo3M2JjOTY1ZDU3OWMzYzYwMzlmMGExN2ViN2MyZTZmYWY1MzhjN2E1ZGU4ZTYwZWM3YTcxOTM2MGQwYTg1N2E5XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZ29sZFwiXG59Iiwie1xuXHRcInhcIjogMjUsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo1NjlhMWYxMTQxNTFiNDUyMTM3M2YzNGJjMTRjMjk2M2E1MDExY2RjMjVhNjU1NGM0OGM3MDhjZDk2ZWJmY1wiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGRlZXBcIlxufSIsIntcblx0XCJ4XCI6IDQ2LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6MjFkYmUzMGIwMjdhY2JjZWI2MTI1NjNiZDg3N2NkN2ViYjcxOWVhNmVkMTM5OTAyN2RjZWU1OGJiOTA0OWQ0YVwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGNyeXN0YWxzXCJcbn0iLCJ7XG5cdFwieFwiOiA2Nyxcblx0XCJ5XCI6IDIsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjVjYmQ5ZjVlYzFlZDAwNzI1OTk5NjQ5MWU2OWZmNjQ5YTMxMDZjZjkyMDIyN2IxYmIzYTcxZWU3YTg5ODYzZlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGZvcmdlXCJcbn0iLCJ7XG5cdFwieFwiOiA4OCxcblx0XCJ5XCI6IDIsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjZiMjBiMjNjMWFhMmJlMDI3MGYwMTZiNGM5MGQ2ZWU2YjgzMzBhMTdjZmVmODc4NjlkNmFkNjBiMmZmYmYzYjVcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBtaW5lc1wiXG59Iiwie1xuXHRcInhcIjogMTA5LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6YTIyMWY4MTNkYWNlZTBmZWY4YzU5Zjc2ODk0ZGJiMjY0MTU0NzhkOWRkZmM0NGMyZTcwOGE2ZDNiNzU0OWJcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBwYXJrXCJcbn0iLCJ7XG5cdFwieFwiOiAxMzAsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo5ZDdlM2IxOWFjNGYzZGVlOWM1Njc3YzEzNTMzM2I5ZDM1YTdmNTY4YjYzZDFlZjRhZGE0YjA2OGI1YTI1XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgc3BpZGVyXCJcbn0iLCJ7XG5cdFwieFwiOiAxNTEsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDpjMzY4N2UyNWM2MzJiY2U4YWE2MWUwZDY0YzI0ZTY5NGMzZWVhNjI5ZWE5NDRmNGNmMzBkY2ZiNGZiY2UwNzFcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBuZXRoZXJcIlxufSJd")
+ if (newButtons != null)
+ buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) })
+ }
+ .position(lastGuiRect.minX + 10, lastGuiRect.minY + 110)
+ .width(lastGuiRect.width - 20)
+ .build()
+ )
}
private fun moveButtons(buttons: List<InventoryButton>): MutableList<InventoryButton> {
@@ -83,14 +149,20 @@ class InventoryButtonEditor(
val movedButtons = mutableListOf<InventoryButton>()
for (button in buttons) {
if ((!button.anchorBottom && !button.anchorRight && button.x > 0 && button.y > 0)) {
- MC.sendChat(tr("firmament.inventory-buttons.button-moved",
- "One of your imported buttons intersects with the inventory and has been moved to the top left."))
- movedButtons.add(button.copy(
- x = 0,
- y = -InventoryButton.dimensions.width,
- anchorRight = false,
- anchorBottom = false
- ))
+ MC.sendChat(
+ tr(
+ "firmament.inventory-buttons.button-moved",
+ "One of your imported buttons intersects with the inventory and has been moved to the top left."
+ )
+ )
+ movedButtons.add(
+ button.copy(
+ x = 0,
+ y = -InventoryButton.dimensions.width,
+ anchorRight = false,
+ anchorBottom = false
+ )
+ )
} else {
newButtons.add(button)
}
@@ -99,9 +171,11 @@ class InventoryButtonEditor(
val zeroRect = Rectangle(0, 0, 1, 1)
for (movedButton in movedButtons) {
fun getPosition(button: InventoryButton, index: Int) =
- button.copy(x = (index % 10) * InventoryButton.dimensions.width,
- y = (index / 10) * -InventoryButton.dimensions.height,
- anchorRight = false, anchorBottom = false)
+ button.copy(
+ x = (index % 10) * InventoryButton.dimensions.width,
+ y = (index / 10) * -InventoryButton.dimensions.height,
+ anchorRight = false, anchorBottom = false
+ )
while (true) {
val newPos = getPosition(movedButton, i++)
val newBounds = newPos.getBounds(zeroRect)
@@ -114,11 +188,23 @@ class InventoryButtonEditor(
return newButtons
}
+ override fun renderBackground(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
+ context.matrices.push()
+ context.matrices.translate(0F, 0F, -15F)
+ super.renderBackground(context, mouseX, mouseY, delta)
+ context.matrices.pop()
+ }
+
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
super.render(context, mouseX, mouseY, delta)
context.matrices.push()
context.matrices.translate(0f, 0f, -10f)
- context.fill(lastGuiRect.minX, lastGuiRect.minY, lastGuiRect.maxX, lastGuiRect.maxY, -1)
+ PanelComponent.DefaultBackgroundRenderer.VANILLA
+ .render(
+ ModernRenderContext(context),
+ lastGuiRect.minX, lastGuiRect.minY,
+ lastGuiRect.width, lastGuiRect.height,
+ )
context.matrices.pop()
for (button in buttons) {
val buttonPosition = button.getBounds(lastGuiRect)
@@ -142,7 +228,8 @@ class InventoryButtonEditor(
if (super.mouseReleased(mouseX, mouseY, button)) return true
val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(Point(mouseX, mouseY)) }
if (clickedButton != null && !justPerformedAClickAction) {
- createPopup(MoulConfigUtils.loadGui("button_editor_fragment", Editor(clickedButton)), Point(mouseX, mouseY))
+ if (InputUtil.isKeyPressed(MC.window.handle, InputUtil.GLFW_KEY_LEFT_CONTROL)) Editor(clickedButton).delete()
+ else createPopup(MoulConfigUtils.loadGui("button_editor_fragment", Editor(clickedButton)), Point(mouseX, mouseY))
return true
}
justPerformedAClickAction = false
@@ -180,14 +267,6 @@ class InventoryButtonEditor(
)
fun getCoordsForMouse(mx: Int, my: Int): AnchoredCoords? {
- if (lastGuiRect.contains(mx, my) || lastGuiRect.contains(
- Point(
- mx + InventoryButton.dimensions.width,
- my + InventoryButton.dimensions.height,
- )
- )
- ) return null
-
val anchorRight = mx > lastGuiRect.maxX
val anchorBottom = my > lastGuiRect.maxY
var offsetX = mx - if (anchorRight) lastGuiRect.maxX else lastGuiRect.minX
@@ -196,7 +275,10 @@ class InventoryButtonEditor(
offsetX = MathHelper.floor(offsetX / 20F) * 20
offsetY = MathHelper.floor(offsetY / 20F) * 20
}
- return AnchoredCoords(anchorRight, anchorBottom, offsetX, offsetY)
+ val rect = InventoryButton(offsetX, offsetY, anchorRight, anchorBottom).getBounds(lastGuiRect)
+ if (rect.intersects(lastGuiRect)) return null
+ val anchoredCoords = AnchoredCoords(anchorRight, anchorBottom, offsetX, offsetY)
+ return anchoredCoords
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt
index d5b5417..ab80d97 100644
--- a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt
+++ b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt
@@ -5,44 +5,50 @@ package moe.nea.firmament.features.inventory.buttons
import me.shedaniel.math.Rectangle
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.client.gui.screen.ingame.HandledScreen
+import net.minecraft.client.gui.screen.ingame.InventoryScreen
+import net.minecraft.text.Text
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HandledScreenClickEvent
import moe.nea.firmament.events.HandledScreenForegroundEvent
import moe.nea.firmament.events.HandledScreenPushREIEvent
-import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.ScreenUtil
+import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.data.DataHolder
import moe.nea.firmament.util.accessors.getRectangle
+import moe.nea.firmament.util.gold
-object InventoryButtons : FirmamentFeature {
- override val identifier: String
- get() = "inventory-buttons"
+object InventoryButtons {
- object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
+ object TConfig : ManagedConfig("inventory-buttons-config", Category.INVENTORY) {
val _openEditor by button("open-editor") {
openEditor()
}
+ val hoverText by toggle("hover-text") { true }
+ val onlyInv by toggle("only-inv") { false }
}
- object DConfig : DataHolder<Data>(serializer(), identifier, ::Data)
+ object DConfig : DataHolder<Data>(serializer(), "inventory-buttons", ::Data)
@Serializable
data class Data(
var buttons: MutableList<InventoryButton> = mutableListOf()
)
+ fun getValidButtons(screen: HandledScreen<*>): Sequence<InventoryButton> {
+ return DConfig.data.buttons.asSequence().filter { button ->
+ button.isValid() && (!TConfig.onlyInv || screen is InventoryScreen)
+ }
+ }
- override val config: ManagedConfig
- get() = TConfig
-
- fun getValidButtons() = DConfig.data.buttons.asSequence().filter { it.isValid() }
@Subscribe
fun onRectangles(it: HandledScreenPushREIEvent) {
val bounds = it.screen.getRectangle()
- for (button in getValidButtons()) {
+ for (button in getValidButtons(it.screen)) {
val buttonBounds = button.getBounds(bounds)
it.block(buttonBounds)
}
@@ -51,7 +57,7 @@ object InventoryButtons : FirmamentFeature {
@Subscribe
fun onClickScreen(it: HandledScreenClickEvent) {
val bounds = it.screen.getRectangle()
- for (button in getValidButtons()) {
+ for (button in getValidButtons(it.screen)) {
val buttonBounds = button.getBounds(bounds)
if (buttonBounds.contains(it.mouseX, it.mouseY)) {
MC.sendCommand(button.command!! /* non null invariant covered by getValidButtons */)
@@ -60,16 +66,36 @@ object InventoryButtons : FirmamentFeature {
}
}
+ var lastHoveredComponent: InventoryButton? = null
+ var lastMouseMove = TimeMark.farPast()
+
@Subscribe
fun onRenderForeground(it: HandledScreenForegroundEvent) {
- val bounds = it.screen.getRectangle()
- for (button in getValidButtons()) {
+ val bounds = it.screen.getRectangle()
+
+ var hoveredComponent: InventoryButton? = null
+ for (button in getValidButtons(it.screen)) {
val buttonBounds = button.getBounds(bounds)
it.context.matrices.push()
it.context.matrices.translate(buttonBounds.minX.toFloat(), buttonBounds.minY.toFloat(), 0F)
button.render(it.context)
it.context.matrices.pop()
+
+ if (buttonBounds.contains(it.mouseX, it.mouseY) && TConfig.hoverText && hoveredComponent == null) {
+ hoveredComponent = button
+ if (lastMouseMove.passedTime() > 0.6.seconds && lastHoveredComponent === button) {
+ it.context.drawTooltip(
+ MC.font,
+ listOf(Text.literal(button.command).gold()),
+ buttonBounds.minX - 15,
+ buttonBounds.maxY + 20,
+ )
+ }
+ }
}
+ if (hoveredComponent !== lastHoveredComponent)
+ lastMouseMove = TimeMark.now()
+ lastHoveredComponent = hoveredComponent
lastRectangle = bounds
}
@@ -78,9 +104,9 @@ object InventoryButtons : FirmamentFeature {
ScreenUtil.setScreenLater(
InventoryButtonEditor(
lastRectangle ?: Rectangle(
- MC.window.scaledWidth / 2 - 100,
- MC.window.scaledHeight / 2 - 100,
- 200, 200,
+ MC.window.scaledWidth / 2 - 88,
+ MC.window.scaledHeight / 2 - 83,
+ 176, 166,
)
)
)
diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt
index 8fad4df..d7346c2 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt
@@ -32,8 +32,8 @@ sealed interface StorageBackingHandle {
StorageBackingHandle, HasBackingScreen
companion object {
- private val enderChestName = "^Ender Chest \\(([1-9])/[1-9]\\)$".toRegex()
- private val backPackName = "^.+Backpack \\(Slot #([0-9]+)\\)$".toRegex()
+ private val enderChestName = "^Ender Chest (?:✦ )?\\(([1-9])/[1-9]\\)$".toRegex()
+ private val backPackName = "^.+Backpack (?:✦ )?\\(Slot #([0-9]+)\\)$".toRegex()
/**
* Parse a screen into a [StorageBackingHandle]. If this returns null it means that the screen is not
diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt
index 2e807de..ec62aa6 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt
@@ -27,12 +27,14 @@ object StorageOverlay : FirmamentFeature {
object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
val alwaysReplace by toggle("always-replace") { true }
+ val outlineActiveStoragePage by toggle("outline-active-page") { false }
val columns by integer("rows", 1, 10) { 3 }
val height by integer("height", 80, 3000) { 3 * 18 * 6 }
val scrollSpeed by integer("scroll-speed", 1, 50) { 10 }
val inverseScroll by toggle("inverse-scroll") { false }
val padding by integer("padding", 1, 20) { 5 }
val margin by integer("margin", 1, 60) { 20 }
+ val itemsBlockScrolling by toggle("block-item-scrolling") { true }
}
fun adjustScrollSpeed(amount: Double): Double {
@@ -100,7 +102,8 @@ object StorageOverlay : FirmamentFeature {
screen.customGui = StorageOverlayCustom(
currentHandler ?: return,
screen,
- storageOverlayScreen ?: (if (TConfig.alwaysReplace) StorageOverlayScreen() else return))
+ storageOverlayScreen ?: (if (TConfig.alwaysReplace) StorageOverlayScreen() else return)
+ )
}
fun rememberContent(handler: StorageBackingHandle?) {
diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt
index 6092e26..81f058e 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt
@@ -9,6 +9,7 @@ import net.minecraft.entity.player.PlayerInventory
import net.minecraft.screen.slot.Slot
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.util.customgui.CustomGui
+import moe.nea.firmament.util.focusedItemStack
class StorageOverlayCustom(
val handler: StorageBackingHandle,
@@ -113,6 +114,8 @@ class StorageOverlayCustom(
horizontalAmount: Double,
verticalAmount: Double
): Boolean {
+ if (screen.focusedItemStack != null && StorageOverlay.TConfig.itemsBlockScrolling)
+ return false
return overview.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
}
diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt
index 633a8fe..84d0f2b 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt
@@ -47,15 +47,18 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
val PLAYER_Y_INSET = 3
val SLOT_SIZE = 18
val PADDING = 10
- val PAGE_WIDTH = SLOT_SIZE * 9
+ val PAGE_SLOTS_WIDTH = SLOT_SIZE * 9
+ val PAGE_WIDTH = PAGE_SLOTS_WIDTH + 4
val HOTBAR_X = 12
val HOTBAR_Y = 67
val MAIN_INVENTORY_Y = 9
val SCROLL_BAR_WIDTH = 8
val SCROLL_BAR_HEIGHT = 16
+ val CONTROL_X_INSET = 3
+ val CONTROL_Y_INSET = 5
val CONTROL_WIDTH = 70
- val CONTROL_BACKGROUND_WIDTH = CONTROL_WIDTH + PLAYER_Y_INSET
- val CONTROL_HEIGHT = 100
+ val CONTROL_BACKGROUND_WIDTH = CONTROL_WIDTH + CONTROL_X_INSET + 1
+ val CONTROL_HEIGHT = 50
}
var isExiting: Boolean = false
@@ -68,13 +71,14 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
val x = width / 2 - overviewWidth / 2
val overviewHeight = minOf(
height - PLAYER_HEIGHT - minOf(80, height / 10),
- StorageOverlay.TConfig.height)
+ StorageOverlay.TConfig.height
+ )
val innerScrollPanelHeight = overviewHeight - PADDING * 2
val y = height / 2 - (overviewHeight + PLAYER_HEIGHT) / 2
val playerX = width / 2 - PLAYER_WIDTH / 2
val playerY = y + overviewHeight - PLAYER_Y_INSET
- val controlX = x - CONTROL_WIDTH
- val controlY = y + overviewHeight / 2 - CONTROL_HEIGHT / 2
+ val controlX = playerX - CONTROL_WIDTH + CONTROL_X_INSET
+ val controlY = playerY - CONTROL_Y_INSET
val totalWidth = overviewWidth
val totalHeight = overviewHeight - PLAYER_Y_INSET + PLAYER_HEIGHT
}
@@ -100,6 +104,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
coerceScroll(StorageOverlay.adjustScrollSpeed(verticalAmount).toFloat())
return true
}
+
fun coerceScroll(offset: Float) {
scroll = (scroll + offset)
.coerceAtMost(getMaxScroll())
@@ -149,21 +154,29 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
fun editPages() {
isExiting = true
- val hs = MC.screen as? HandledScreen<*>
- if (StorageBackingHandle.fromScreen(hs) is StorageBackingHandle.Overview) {
- hs.customGui = null
- } else {
- MC.sendCommand("storage")
+ MC.instance.send {
+ val hs = MC.screen as? HandledScreen<*>
+ if (StorageBackingHandle.fromScreen(hs) is StorageBackingHandle.Overview) {
+ hs.customGui = null
+ hs.init(MC.instance, width, height)
+ } else {
+ MC.sendCommand("storage")
+ }
}
}
val guiContext = GuiContext(EmptyComponent())
private val knobStub = EmptyComponent()
- val editButton = FirmButtonComponent(TextComponent(tr("firmament.storage-overlay.edit-pages", "Edit Pages").string), action = ::editPages)
+ val editButton = FirmButtonComponent(
+ TextComponent(tr("firmament.storage-overlay.edit-pages", "Edit Pages").string),
+ action = ::editPages
+ )
val searchText = Property.of("") // TODO: sync with REI
- val searchField = TextFieldComponent(searchText, 100, GetSetter.constant(true),
- tr("firmament.storage-overlay.search.suggestion", "Search...").string,
- IMinecraft.instance.defaultFontRenderer)
+ val searchField = TextFieldComponent(
+ searchText, 100, GetSetter.constant(true),
+ tr("firmament.storage-overlay.search.suggestion", "Search...").string,
+ IMinecraft.instance.defaultFontRenderer
+ )
val controlComponent = PanelComponent(
ColumnComponent(
searchField,
@@ -186,25 +199,31 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
controllerBackground,
measurements.controlX,
measurements.controlY,
- CONTROL_BACKGROUND_WIDTH, CONTROL_HEIGHT)
+ CONTROL_BACKGROUND_WIDTH, CONTROL_HEIGHT
+ )
context.drawMCComponentInPlace(
controlComponent,
measurements.controlX, measurements.controlY,
CONTROL_WIDTH, CONTROL_HEIGHT,
- mouseX, mouseY)
+ mouseX, mouseY
+ )
}
fun drawBackgrounds(context: DrawContext) {
- context.drawGuiTexture(upperBackgroundSprite,
- measurements.x,
- measurements.y,
- measurements.overviewWidth,
- measurements.overviewHeight)
- context.drawGuiTexture(playerInventorySprite,
- measurements.playerX,
- measurements.playerY,
- PLAYER_WIDTH,
- PLAYER_HEIGHT)
+ context.drawGuiTexture(
+ upperBackgroundSprite,
+ measurements.x,
+ measurements.y,
+ measurements.overviewWidth,
+ measurements.overviewHeight
+ )
+ context.drawGuiTexture(
+ playerInventorySprite,
+ measurements.playerX,
+ measurements.playerY,
+ PLAYER_WIDTH,
+ PLAYER_HEIGHT
+ )
}
fun getPlayerInventorySlotPosition(int: Int): Pair<Int, Int> {
@@ -218,7 +237,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
}
fun drawPlayerInventory(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
- val items = MC.player?.inventory?.main ?: return
+ val items = MC.player?.inventory?.mainStacks ?: return
items.withIndex().forEach { (index, item) ->
val (x, y) = getPlayerInventorySlotPosition(index)
context.drawItem(item, x, y, 0)
@@ -227,17 +246,21 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
}
fun getScrollBarRect(): Rectangle {
- return Rectangle(measurements.x + PADDING + measurements.innerScrollPanelWidth + PADDING,
- measurements.y + PADDING,
- SCROLL_BAR_WIDTH,
- measurements.innerScrollPanelHeight)
+ return Rectangle(
+ measurements.x + PADDING + measurements.innerScrollPanelWidth + PADDING,
+ measurements.y + PADDING,
+ SCROLL_BAR_WIDTH,
+ measurements.innerScrollPanelHeight
+ )
}
fun getScrollPanelInner(): Rectangle {
- return Rectangle(measurements.x + PADDING,
- measurements.y + PADDING,
- measurements.innerScrollPanelWidth,
- measurements.innerScrollPanelHeight)
+ return Rectangle(
+ measurements.x + PADDING,
+ measurements.y + PADDING,
+ measurements.innerScrollPanelWidth,
+ measurements.innerScrollPanelHeight
+ )
}
fun createScissors(context: DrawContext) {
@@ -257,12 +280,13 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
createScissors(context)
val data = StorageOverlay.Data.data ?: StorageData()
layoutedForEach(data) { rect, page, inventory ->
- drawPage(context,
- rect.x,
- rect.y,
- page, inventory,
- if (excluding == page) slots else null,
- slotOffset
+ drawPage(
+ context,
+ rect.x,
+ rect.y,
+ page, inventory,
+ if (excluding == page) slots else null,
+ slotOffset
)
}
context.disableScissor()
@@ -282,11 +306,13 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
knobGrabbed = false
return true
}
- if (clickMCComponentInPlace(controlComponent,
- measurements.controlX, measurements.controlY,
- CONTROL_WIDTH, CONTROL_HEIGHT,
- mouseX.toInt(), mouseY.toInt(),
- MouseEvent.Click(button, false))
+ if (clickMCComponentInPlace(
+ controlComponent,
+ measurements.controlX, measurements.controlY,
+ CONTROL_WIDTH, CONTROL_HEIGHT,
+ mouseX.toInt(), mouseY.toInt(),
+ MouseEvent.Click(button, false)
+ )
) return true
return super.mouseReleased(mouseX, mouseY, button)
}
@@ -322,11 +348,13 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
knobGrabbed = true
return true
}
- if (clickMCComponentInPlace(controlComponent,
- measurements.controlX, measurements.controlY,
- CONTROL_WIDTH, CONTROL_HEIGHT,
- mouseX.toInt(), mouseY.toInt(),
- MouseEvent.Click(button, true))
+ if (clickMCComponentInPlace(
+ controlComponent,
+ measurements.controlX, measurements.controlY,
+ CONTROL_WIDTH, CONTROL_HEIGHT,
+ mouseX.toInt(), mouseY.toInt(),
+ MouseEvent.Click(button, true)
+ )
) return true
return false
}
@@ -357,6 +385,10 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
return super.keyReleased(keyCode, scanCode, modifiers)
}
+ override fun shouldCloseOnEsc(): Boolean {
+ return this === MC.screen // Fixes this UI closing the handled screen on Escape press.
+ }
+
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
if (typeMCComponentInPlace(
controlComponent,
@@ -416,7 +448,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
val filter = getFilteredPages()
for ((page, inventory) in data.storageInventories.entries) {
if (page !in filter) continue
- val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 4 + textRenderer.fontHeight }
+ val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 6 + textRenderer.fontHeight }
?: 18
maxHeight = maxOf(maxHeight, currentHeight)
val rect = Rectangle(
@@ -448,22 +480,41 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
val inv = inventory.inventory
if (inv == null) {
context.drawGuiTexture(upperBackgroundSprite, x, y, PAGE_WIDTH, 18)
- context.drawText(textRenderer,
- Text.literal("TODO: open this page"),
- x + 4,
- y + 4,
- -1,
- true)
+ context.drawText(
+ textRenderer,
+ Text.literal("TODO: open this page"),
+ x + 4,
+ y + 4,
+ -1,
+ true
+ )
return 18
}
assertTrueOr(slots == null || slots.size == inv.stacks.size) { return 0 }
val name = page.defaultName()
- context.drawText(textRenderer, Text.literal(name), x + 4, y + 2,
- if (slots == null) 0xFFFFFFFF.toInt() else 0xFFFFFF00.toInt(), true)
- context.drawGuiTexture(slotRowSprite, x, y + 4 + textRenderer.fontHeight, PAGE_WIDTH, inv.rows * SLOT_SIZE)
+ val pageHeight = inv.rows * SLOT_SIZE + 8 + textRenderer.fontHeight
+ if (slots != null && StorageOverlay.TConfig.outlineActiveStoragePage)
+ context.drawBorder(
+ x,
+ y + 3 + textRenderer.fontHeight,
+ PAGE_WIDTH,
+ inv.rows * SLOT_SIZE + 4,
+ 0xFFFF00FF.toInt()
+ )
+ context.drawText(
+ textRenderer, Text.literal(name), x + 6, y + 3,
+ if (slots == null) 0xFFFFFFFF.toInt() else 0xFFFFFF00.toInt(), true
+ )
+ context.drawGuiTexture(
+ slotRowSprite,
+ x + 2,
+ y + 5 + textRenderer.fontHeight,
+ PAGE_SLOTS_WIDTH,
+ inv.rows * SLOT_SIZE
+ )
inv.stacks.forEachIndexed { index, stack ->
- val slotX = (index % 9) * SLOT_SIZE + x + 1
- val slotY = (index / 9) * SLOT_SIZE + y + 4 + textRenderer.fontHeight + 1
+ val slotX = (index % 9) * SLOT_SIZE + x + 3
+ val slotY = (index / 9) * SLOT_SIZE + y + 5 + textRenderer.fontHeight + 1
val fakeSlot = FakeSlot(stack, slotX, slotY)
if (slots == null) {
SlotRenderEvents.Before.publish(SlotRenderEvents.Before(context, fakeSlot))
@@ -476,22 +527,29 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
slot.y = slotY - slotOffset.y
}
}
- return inv.rows * SLOT_SIZE + 4 + textRenderer.fontHeight
+ return pageHeight + 6
}
fun getBounds(): List<Rectangle> {
return listOf(
- Rectangle(measurements.x,
- measurements.y,
- measurements.overviewWidth,
- measurements.overviewHeight),
- Rectangle(measurements.playerX,
- measurements.playerY,
- PLAYER_WIDTH,
- PLAYER_HEIGHT),
- Rectangle(measurements.controlX,
- measurements.controlY,
- CONTROL_WIDTH,
- CONTROL_HEIGHT))
+ Rectangle(
+ measurements.x,
+ measurements.y,
+ measurements.overviewWidth,
+ measurements.overviewHeight
+ ),
+ Rectangle(
+ measurements.playerX,
+ measurements.playerY,
+ PLAYER_WIDTH,
+ PLAYER_HEIGHT
+ ),
+ Rectangle(
+ measurements.controlX,
+ measurements.controlY,
+ CONTROL_WIDTH,
+ CONTROL_HEIGHT
+ )
+ )
}
}
diff --git a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt
index 3b86184..d99acd7 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt
@@ -11,6 +11,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
+import kotlin.jvm.optionals.getOrNull
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtIo
@@ -42,15 +43,15 @@ data class VirtualInventory(
override fun deserialize(decoder: Decoder): VirtualInventory {
val s = decoder.decodeString()
val n = NbtIo.readCompressed(ByteArrayInputStream(s.decodeBase64Bytes()), NbtSizeTracker.of(100_000_000))
- val items = n.getList(INVENTORY, NbtCompound.COMPOUND_TYPE.toInt())
+ val items = n.getList(INVENTORY).getOrNull()
val ops = getOps()
- return VirtualInventory(items.map {
+ return VirtualInventory(items?.map {
it as NbtCompound
if (it.isEmpty) ItemStack.EMPTY
else ErrorUtil.catch("Could not deserialize item") {
ItemStack.CODEC.parse(ops, it).orThrow
}.or { ItemStack.EMPTY }
- })
+ } ?: listOf())
}
fun getOps() = TolerantRegistriesOps(NbtOps.INSTANCE, MC.currentOrDefaultRegistries)
diff --git a/src/main/kotlin/features/items/EtherwarpOverlay.kt b/src/main/kotlin/features/items/EtherwarpOverlay.kt
new file mode 100644
index 0000000..83e533c
--- /dev/null
+++ b/src/main/kotlin/features/items/EtherwarpOverlay.kt
@@ -0,0 +1,49 @@
+package moe.nea.firmament.features.items
+
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.MC
+import net.minecraft.util.hit.BlockHitResult
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.util.extraAttributes
+import moe.nea.firmament.util.render.RenderInWorldContext
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.SkyBlockItems
+
+object EtherwarpOverlay : FirmamentFeature {
+ override val identifier: String
+ get() = "etherwarp-overlay"
+
+ object TConfig : ManagedConfig(identifier, Category.ITEMS) {
+ var etherwarpOverlay by toggle("etherwarp-overlay") { false }
+ var cube by toggle("cube") { true }
+ var wireframe by toggle("wireframe") { false }
+ }
+
+ override val config: ManagedConfig
+ get() = TConfig
+
+
+ @Subscribe
+ fun renderEtherwarpOverlay(event: WorldRenderLastEvent) {
+ if (!TConfig.etherwarpOverlay) return
+ val player = MC.player ?: return
+ val world = player.world
+ val camera = MC.camera ?: return
+ val heldItem = MC.stackInHand
+ if (heldItem.skyBlockId !in listOf(SkyBlockItems.ASPECT_OF_THE_VOID, SkyBlockItems.ASPECT_OF_THE_END)) return
+ if (!heldItem.extraAttributes.contains("ethermerge")) return
+
+ val hitResult = camera.raycast(61.0, 0.0f, false)
+ if (hitResult !is BlockHitResult) return
+ val blockPos = hitResult.blockPos
+ if (camera.squaredDistanceTo(blockPos.toCenterPos()) > 61 * 61) return
+ if (!world.getBlockState(blockPos.up()).isAir) return
+ if (!world.getBlockState(blockPos.up(2)).isAir) return
+ RenderInWorldContext.renderInWorld(event) {
+ if (TConfig.cube) block(blockPos, 0xFFFFFF00.toInt())
+ if (TConfig.wireframe) wireframeCube(blockPos, 10f)
+ }
+ }
+}
diff --git a/src/main/kotlin/features/macros/ComboProcessor.kt b/src/main/kotlin/features/macros/ComboProcessor.kt
new file mode 100644
index 0000000..5c5ac0e
--- /dev/null
+++ b/src/main/kotlin/features/macros/ComboProcessor.kt
@@ -0,0 +1,114 @@
+package moe.nea.firmament.features.macros
+
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.client.util.InputUtil
+import net.minecraft.text.Text
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HudRenderEvent
+import moe.nea.firmament.events.TickEvent
+import moe.nea.firmament.events.WorldKeyboardEvent
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.tr
+
+object ComboProcessor {
+
+ var rootTrie: Branch = Branch(mapOf())
+ private set
+
+ var activeTrie: Branch = rootTrie
+ private set
+
+ var isInputting = false
+ var lastInput = TimeMark.farPast()
+ val breadCrumbs = mutableListOf<SavedKeyBinding>()
+
+ init {
+ val f = SavedKeyBinding(InputUtil.GLFW_KEY_F)
+ val one = SavedKeyBinding(InputUtil.GLFW_KEY_1)
+ val two = SavedKeyBinding(InputUtil.GLFW_KEY_2)
+ setActions(
+ MacroData.DConfig.data.comboActions
+ )
+ }
+
+ fun setActions(actions: List<ComboKeyAction>) {
+ rootTrie = KeyComboTrie.fromComboList(actions)
+ reset()
+ }
+
+ fun reset() {
+ activeTrie = rootTrie
+ lastInput = TimeMark.now()
+ isInputting = false
+ breadCrumbs.clear()
+ }
+
+ @Subscribe
+ fun onTick(event: TickEvent) {
+ if (isInputting && lastInput.passedTime() > 3.seconds)
+ reset()
+ }
+
+
+ @Subscribe
+ fun onRender(event: HudRenderEvent) {
+ if (!isInputting) return
+ if (!event.isRenderingHud) return
+ event.context.matrices.push()
+ val width = 120
+ event.context.matrices.translate(
+ (MC.window.scaledWidth - width) / 2F,
+ (MC.window.scaledHeight) / 2F + 8,
+ 0F
+ )
+ val breadCrumbText = breadCrumbs.joinToString(" > ")
+ event.context.drawText(
+ MC.font,
+ tr("firmament.combo.active", "Current Combo: ").append(breadCrumbText),
+ 0,
+ 0,
+ -1,
+ true
+ )
+ event.context.matrices.translate(0F, MC.font.fontHeight + 2F, 0F)
+ for ((key, value) in activeTrie.nodes) {
+ event.context.drawText(
+ MC.font,
+ Text.literal("$breadCrumbText > $key: ").append(value.label),
+ 0,
+ 0,
+ -1,
+ true
+ )
+ event.context.matrices.translate(0F, MC.font.fontHeight + 1F, 0F)
+ }
+ event.context.matrices.pop()
+ }
+
+ @Subscribe
+ fun onKeyBinding(event: WorldKeyboardEvent) {
+ val nextEntry = activeTrie.nodes.entries
+ .find { event.matches(it.key) }
+ if (nextEntry == null) {
+ reset()
+ return
+ }
+ event.cancel()
+ breadCrumbs.add(nextEntry.key)
+ lastInput = TimeMark.now()
+ isInputting = true
+ val value = nextEntry.value
+ when (value) {
+ is Branch -> {
+ activeTrie = value
+ }
+
+ is Leaf -> {
+ value.execute()
+ reset()
+ }
+ }.let { }
+ }
+}
diff --git a/src/main/kotlin/features/macros/HotkeyAction.kt b/src/main/kotlin/features/macros/HotkeyAction.kt
new file mode 100644
index 0000000..011f797
--- /dev/null
+++ b/src/main/kotlin/features/macros/HotkeyAction.kt
@@ -0,0 +1,40 @@
+package moe.nea.firmament.features.macros
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import net.minecraft.text.Text
+import moe.nea.firmament.util.MC
+
+@Serializable
+sealed interface HotkeyAction {
+ // TODO: execute
+ val label: Text
+ fun execute()
+}
+
+@Serializable
+@SerialName("command")
+data class CommandAction(val command: String) : HotkeyAction {
+ override val label: Text
+ get() = Text.literal("/$command")
+
+ override fun execute() {
+ MC.sendCommand(command)
+ }
+}
+
+// Mit onscreen anzeige:
+// F -> 1 /equipment
+// F -> 2 /wardrobe
+// Bei Combos: Keys buffern! (für wardrobe hotkeys beispielsweiße)
+
+// Radial menu
+// Hold F
+// Weight (mach eins doppelt so groß)
+// /equipment
+// /wardrobe
+
+// Bei allen: Filter!
+// - Nur in Dungeons / andere Insel
+// - Nur wenn ich Item X im inventar habe (fishing rod)
+
diff --git a/src/main/kotlin/features/macros/KeyComboTrie.kt b/src/main/kotlin/features/macros/KeyComboTrie.kt
new file mode 100644
index 0000000..452bc56
--- /dev/null
+++ b/src/main/kotlin/features/macros/KeyComboTrie.kt
@@ -0,0 +1,73 @@
+package moe.nea.firmament.features.macros
+
+import kotlinx.serialization.Serializable
+import net.minecraft.text.Text
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.util.ErrorUtil
+
+sealed interface KeyComboTrie {
+ val label: Text
+
+ companion object {
+ fun fromComboList(
+ combos: List<ComboKeyAction>,
+ ): Branch {
+ val root = Branch(mutableMapOf())
+ for (combo in combos) {
+ var p = root
+ if (combo.keys.isEmpty()) {
+ ErrorUtil.softUserError("Key Combo for ${combo.action.label.string} is empty")
+ continue
+ }
+ for ((index, key) in combo.keys.withIndex()) {
+ val m = (p.nodes as MutableMap)
+ if (index == combo.keys.lastIndex) {
+ if (key in m) {
+ ErrorUtil.softUserError("Overlapping actions found for ${combo.keys.joinToString(" > ")} (another action ${m[key]} already exists).")
+ break
+ }
+
+ m[key] = Leaf(combo.action)
+ } else {
+ val c = m.getOrPut(key) { Branch(mutableMapOf()) }
+ if (c !is Branch) {
+ ErrorUtil.softUserError("Overlapping actions found for ${combo.keys} (final node exists at index $index) through another action already")
+ break
+ } else {
+ p = c
+ }
+ }
+ }
+ }
+ return root
+ }
+ }
+}
+
+@Serializable
+data class MacroWheel(
+ val key: SavedKeyBinding,
+ val options: List<HotkeyAction>
+)
+
+@Serializable
+data class ComboKeyAction(
+ val action: HotkeyAction,
+ val keys: List<SavedKeyBinding>,
+)
+
+data class Leaf(val action: HotkeyAction) : KeyComboTrie {
+ override val label: Text
+ get() = action.label
+
+ fun execute() {
+ action.execute()
+ }
+}
+
+data class Branch(
+ val nodes: Map<SavedKeyBinding, KeyComboTrie>
+) : KeyComboTrie {
+ override val label: Text
+ get() = Text.literal("...") // TODO: better labels
+}
diff --git a/src/main/kotlin/features/macros/MacroData.kt b/src/main/kotlin/features/macros/MacroData.kt
new file mode 100644
index 0000000..91de423
--- /dev/null
+++ b/src/main/kotlin/features/macros/MacroData.kt
@@ -0,0 +1,12 @@
+package moe.nea.firmament.features.macros
+
+import kotlinx.serialization.Serializable
+import moe.nea.firmament.util.data.DataHolder
+
+@Serializable
+data class MacroData(
+ var comboActions: List<ComboKeyAction> = listOf(),
+ var wheels: List<MacroWheel> = listOf(),
+) {
+ object DConfig : DataHolder<MacroData>(kotlinx.serialization.serializer(), "macros", ::MacroData)
+}
diff --git a/src/main/kotlin/features/macros/MacroUI.kt b/src/main/kotlin/features/macros/MacroUI.kt
new file mode 100644
index 0000000..8c22c5c
--- /dev/null
+++ b/src/main/kotlin/features/macros/MacroUI.kt
@@ -0,0 +1,285 @@
+package moe.nea.firmament.features.macros
+
+import io.github.notenoughupdates.moulconfig.gui.CloseEventListener
+import io.github.notenoughupdates.moulconfig.observer.ObservableList
+import io.github.notenoughupdates.moulconfig.xml.Bind
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.gui.config.AllConfigsGui.toObservableList
+import moe.nea.firmament.gui.config.KeyBindingStateManager
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.MoulConfigUtils
+import moe.nea.firmament.util.ScreenUtil
+
+class MacroUI {
+
+
+ companion object {
+ @Subscribe
+ fun onCommands(event: CommandEvent.SubCommand) {
+ // TODO: add button in config
+ event.subcommand("macros") {
+ thenExecute {
+ ScreenUtil.setScreenLater(MoulConfigUtils.loadScreen("config/macros/index", MacroUI(), null))
+ }
+ }
+ }
+
+ }
+
+ @field:Bind("combos")
+ val combos = Combos()
+
+ @field:Bind("wheels")
+ val wheels = Wheels()
+ var dontSave = false
+
+ @Bind
+ fun beforeClose(): CloseEventListener.CloseAction {
+ if (!dontSave)
+ save()
+ return CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE
+ }
+
+ fun save() {
+ MacroData.DConfig.data.comboActions = combos.actions.map { it.asSaveable() }
+ MacroData.DConfig.data.wheels = wheels.wheels.map { it.asSaveable() }
+ MacroData.DConfig.markDirty()
+ RadialMacros.setWheels(MacroData.DConfig.data.wheels)
+ ComboProcessor.setActions(MacroData.DConfig.data.comboActions)
+ }
+
+ fun discard() {
+ dontSave = true
+ MC.screen?.close()
+ }
+
+ class Command(
+ @field:Bind("text")
+ var text: String,
+ val parent: Wheel,
+ ) {
+ @Bind
+ fun delete() {
+ parent.editableCommands.removeIf { it === this }
+ parent.editableCommands.update()
+ parent.commands.update()
+ }
+
+ fun asCommandAction() = CommandAction(text)
+ }
+
+ inner class Wheel(
+ val parent: Wheels,
+ var binding: SavedKeyBinding,
+ commands: List<CommandAction>,
+ ) {
+
+ fun asSaveable(): MacroWheel {
+ return MacroWheel(binding, commands.map { it.asCommandAction() })
+ }
+
+ @Bind("keyCombo")
+ fun text() = binding.format().string
+
+ @field:Bind("commands")
+ val commands = commands.mapTo(ObservableList(mutableListOf())) { Command(it.command, this) }
+
+ @field:Bind("editableCommands")
+ val editableCommands = this.commands.toObservableList()
+
+ @Bind
+ fun addOption() {
+ editableCommands.add(Command("", this))
+ }
+
+ @Bind
+ fun back() {
+ MC.screen?.close()
+ }
+
+ @Bind
+ fun edit() {
+ MC.screen = MoulConfigUtils.loadScreen("config/macros/editor_wheel", this, MC.screen)
+ }
+
+ @Bind
+ fun delete() {
+ parent.wheels.removeIf { it === this }
+ parent.wheels.update()
+ }
+
+ val sm = KeyBindingStateManager(
+ { binding },
+ { binding = it },
+ ::blur,
+ ::requestFocus
+ )
+
+ @field:Bind
+ val button = sm.createButton()
+
+ init {
+ sm.updateLabel()
+ }
+
+ fun blur() {
+ button.blur()
+ }
+
+
+ fun requestFocus() {
+ button.requestFocus()
+ }
+ }
+
+ inner class Wheels {
+ @field:Bind("wheels")
+ val wheels: ObservableList<Wheel> = MacroData.DConfig.data.wheels.mapTo(ObservableList(mutableListOf())) {
+ Wheel(this, it.key, it.options.map { CommandAction((it as CommandAction).command) })
+ }
+
+ @Bind
+ fun discard() {
+ this@MacroUI.discard()
+ }
+
+ @Bind
+ fun saveAndClose() {
+ this@MacroUI.saveAndClose()
+ }
+
+ @Bind
+ fun save() {
+ this@MacroUI.save()
+ }
+
+ @Bind
+ fun addWheel() {
+ wheels.add(Wheel(this, SavedKeyBinding.unbound(), listOf()))
+ }
+ }
+
+ fun saveAndClose() {
+ save()
+ MC.screen?.close()
+ }
+
+ inner class Combos {
+ @field:Bind("actions")
+ val actions: ObservableList<ActionEditor> = ObservableList(
+ MacroData.DConfig.data.comboActions.mapTo(mutableListOf()) {
+ ActionEditor(it, this)
+ }
+ )
+
+ @Bind
+ fun addCommand() {
+ actions.add(
+ ActionEditor(
+ ComboKeyAction(
+ CommandAction("ac Hello from a Firmament Hotkey"),
+ listOf()
+ ),
+ this
+ )
+ )
+ }
+
+ @Bind
+ fun discard() {
+ this@MacroUI.discard()
+ }
+
+ @Bind
+ fun saveAndClose() {
+ this@MacroUI.saveAndClose()
+ }
+
+ @Bind
+ fun save() {
+ this@MacroUI.save()
+ }
+ }
+
+ class KeyBindingEditor(var binding: SavedKeyBinding, val parent: ActionEditor) {
+ val sm = KeyBindingStateManager(
+ { binding },
+ { binding = it },
+ ::blur,
+ ::requestFocus
+ )
+
+ @field:Bind
+ val button = sm.createButton()
+
+ init {
+ sm.updateLabel()
+ }
+
+ fun blur() {
+ button.blur()
+ }
+
+
+ fun requestFocus() {
+ button.requestFocus()
+ }
+
+ @Bind
+ fun delete() {
+ parent.combo.removeIf { it === this }
+ parent.combo.update()
+ }
+ }
+
+ class ActionEditor(val action: ComboKeyAction, val parent: Combos) {
+ fun asSaveable(): ComboKeyAction {
+ return ComboKeyAction(
+ CommandAction(command),
+ combo.map { it.binding }
+ )
+ }
+
+ @field:Bind("command")
+ var command: String = (action.action as CommandAction).command
+
+ @field:Bind("combo")
+ val combo = action.keys.map { KeyBindingEditor(it, this) }.toObservableList()
+
+ @Bind
+ fun formattedCombo() =
+ combo.joinToString(" > ") { it.binding.toString() }
+
+ @Bind
+ fun addStep() {
+ combo.add(KeyBindingEditor(SavedKeyBinding.unbound(), this))
+ }
+
+ @Bind
+ fun back() {
+ MC.screen?.close()
+ }
+
+ @Bind
+ fun delete() {
+ parent.actions.removeIf { it === this }
+ parent.actions.update()
+ }
+
+ @Bind
+ fun edit() {
+ MC.screen = MoulConfigUtils.loadScreen("config/macros/editor_combo", this, MC.screen)
+ }
+ }
+}
+
+private fun <T> ObservableList<T>.setAll(ts: Collection<T>) {
+ val observer = this.observer
+ this.clear()
+ this.addAll(ts)
+ this.observer = observer
+ this.update()
+}
diff --git a/src/main/kotlin/features/macros/RadialMenu.kt b/src/main/kotlin/features/macros/RadialMenu.kt
new file mode 100644
index 0000000..2e09c44
--- /dev/null
+++ b/src/main/kotlin/features/macros/RadialMenu.kt
@@ -0,0 +1,149 @@
+package moe.nea.firmament.features.macros
+
+import org.joml.Vector2f
+import util.render.CustomRenderLayers
+import kotlin.math.atan2
+import kotlin.math.cos
+import kotlin.math.sin
+import kotlin.math.sqrt
+import net.minecraft.client.gui.DrawContext
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HudRenderEvent
+import moe.nea.firmament.events.TickEvent
+import moe.nea.firmament.events.WorldKeyboardEvent
+import moe.nea.firmament.events.WorldMouseMoveEvent
+import moe.nea.firmament.features.macros.RadialMenuViewer.RadialMenu
+import moe.nea.firmament.features.macros.RadialMenuViewer.RadialMenuOption
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.render.RenderCircleProgress
+import moe.nea.firmament.util.render.drawLine
+import moe.nea.firmament.util.render.lerpAngle
+import moe.nea.firmament.util.render.wrapAngle
+import moe.nea.firmament.util.render.τ
+
+object RadialMenuViewer {
+ interface RadialMenu {
+ val key: SavedKeyBinding
+ val options: List<RadialMenuOption>
+ }
+
+ interface RadialMenuOption {
+ val isEnabled: Boolean
+ fun resolve()
+ fun renderSlice(drawContext: DrawContext)
+ }
+
+ var activeMenu: RadialMenu? = null
+ set(value) {
+ field = value
+ delta = Vector2f(0F, 0F)
+ }
+ var delta = Vector2f(0F, 0F)
+ val maxSelectionSize = 100F
+
+ @Subscribe
+ fun onMouseMotion(event: WorldMouseMoveEvent) {
+ val menu = activeMenu ?: return
+ event.cancel()
+ delta.add(event.deltaX.toFloat(), event.deltaY.toFloat())
+ val m = delta.lengthSquared()
+ if (m > maxSelectionSize * maxSelectionSize) {
+ delta.mul(maxSelectionSize / sqrt(m))
+ }
+ }
+
+ val INNER_CIRCLE_RADIUS = 16
+
+ @Subscribe
+ fun onRender(event: HudRenderEvent) {
+ val menu = activeMenu ?: return
+ val mat = event.context.matrices
+ mat.push()
+ mat.translate(
+ (MC.window.scaledWidth) / 2F,
+ (MC.window.scaledHeight) / 2F,
+ 0F
+ )
+ val sliceWidth = (τ / menu.options.size).toFloat()
+ var selectedAngle = wrapAngle(atan2(delta.y, delta.x))
+ if (delta.lengthSquared() < INNER_CIRCLE_RADIUS * INNER_CIRCLE_RADIUS)
+ selectedAngle = Float.NaN
+ for ((idx, option) in menu.options.withIndex()) {
+ val range = (sliceWidth * idx)..(sliceWidth * (idx + 1))
+ mat.push()
+ mat.scale(64F, 64F, 1F)
+ val cutout = INNER_CIRCLE_RADIUS / 64F / 2
+ RenderCircleProgress.renderCircularSlice(
+ event.context,
+ CustomRenderLayers.TRANSLUCENT_CIRCLE_GUI,
+ 0F, 1F, 0F, 1F,
+ range,
+ color = if (selectedAngle in range) 0x70A0A0A0 else 0x70FFFFFF,
+ innerCutoutRadius = cutout
+ )
+ mat.pop()
+ mat.push()
+ val centreAngle = lerpAngle(range.start, range.endInclusive, 0.5F)
+ val vec = Vector2f(cos(centreAngle), sin(centreAngle)).mul(40F)
+ mat.translate(vec.x, vec.y, 0F)
+ option.renderSlice(event.context)
+ mat.pop()
+ }
+ event.context.drawLine(1, 1, delta.x.toInt(), delta.y.toInt(), me.shedaniel.math.Color.ofOpaque(0x00FF00))
+ mat.pop()
+ }
+
+ @Subscribe
+ fun onTick(event: TickEvent) {
+ val menu = activeMenu ?: return
+ if (!menu.key.isPressed(true)) {
+ val angle = atan2(delta.y, delta.x)
+
+ val choiceIndex = (wrapAngle(angle) * menu.options.size / τ).toInt()
+ val choice = menu.options[choiceIndex]
+ val selectedAny = delta.lengthSquared() > INNER_CIRCLE_RADIUS * INNER_CIRCLE_RADIUS
+ activeMenu = null
+ if (selectedAny)
+ choice.resolve()
+ }
+ }
+
+}
+
+object RadialMacros {
+ var wheels = MacroData.DConfig.data.wheels
+ private set
+
+ fun setWheels(wheels: List<MacroWheel>) {
+ this.wheels = wheels
+ RadialMenuViewer.activeMenu = null
+ }
+
+ @Subscribe
+ fun onOpen(event: WorldKeyboardEvent) {
+ if (RadialMenuViewer.activeMenu != null) return
+ wheels.forEach { wheel ->
+ if (event.matches(wheel.key, atLeast = true)) {
+ class R(val action: HotkeyAction) : RadialMenuOption {
+ override val isEnabled: Boolean
+ get() = true
+
+ override fun resolve() {
+ action.execute()
+ }
+
+ override fun renderSlice(drawContext: DrawContext) {
+ drawContext.drawCenteredTextWithShadow(MC.font, action.label, 0, 0, -1)
+ }
+ }
+ RadialMenuViewer.activeMenu = object : RadialMenu {
+ override val key: SavedKeyBinding
+ get() = wheel.key
+ override val options: List<RadialMenuOption> =
+ wheel.options.map { R(it) }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/features/mining/PickaxeAbility.kt b/src/main/kotlin/features/mining/PickaxeAbility.kt
index 1737969..430bae0 100644
--- a/src/main/kotlin/features/mining/PickaxeAbility.kt
+++ b/src/main/kotlin/features/mining/PickaxeAbility.kt
@@ -1,9 +1,13 @@
package moe.nea.firmament.features.mining
import java.util.regex.Pattern
+import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
+import net.minecraft.client.MinecraftClient
+import net.minecraft.client.toast.SystemToast
import net.minecraft.item.ItemStack
+import net.minecraft.text.Text
import net.minecraft.util.DyeColor
import net.minecraft.util.Hand
import net.minecraft.util.Identifier
@@ -47,6 +51,7 @@ object PickaxeAbility : FirmamentFeature {
object TConfig : ManagedConfig(identifier, Category.MINING) {
val cooldownEnabled by toggle("ability-cooldown") { false }
val cooldownScale by integer("ability-scale", 16, 64) { 16 }
+ val cooldownReadyToast by toggle("ability-cooldown-toast") { false }
val drillFuelBar by toggle("fuel-bar") { true }
val blockOnPrivateIsland by choice(
"block-on-dynamic",
@@ -140,9 +145,9 @@ object PickaxeAbility : FirmamentFeature {
}
} ?: return
val extra = it.item.extraAttributes
- if (!extra.contains("drill_fuel")) return
- val fuel = extra.getInt("drill_fuel")
- val percentage = fuel / maxFuel.toFloat()
+ val fuel = extra.getInt("drill_fuel").getOrNull() ?: return
+ var percentage = fuel / maxFuel.toFloat()
+ if (percentage > 1f) percentage = 1f
it.barOverride = DurabilityBarEvent.DurabilityBar(
lerp(
DyeColor.RED.toShedaniel(),
@@ -170,6 +175,11 @@ object PickaxeAbility : FirmamentFeature {
nowAvailable.useMatch(it.unformattedString) {
val ability = group("name")
lastUsage[ability] = TimeMark.farPast()
+ if (!TConfig.cooldownReadyToast) return
+ val mc: MinecraftClient = MinecraftClient.getInstance()
+ mc.toastManager.add(
+ SystemToast.create(mc, SystemToast.Type.NARRATOR_TOGGLE, tr("firmament.pickaxe.ability-ready","Pickaxe Cooldown"), tr("firmament.pickaxe.ability-ready.desc", "Pickaxe ability is ready!"))
+ )
}
}
diff --git a/src/main/kotlin/features/misc/CustomCapes.kt b/src/main/kotlin/features/misc/CustomCapes.kt
new file mode 100644
index 0000000..dc5187a
--- /dev/null
+++ b/src/main/kotlin/features/misc/CustomCapes.kt
@@ -0,0 +1,192 @@
+package moe.nea.firmament.features.misc
+
+import com.mojang.blaze3d.systems.RenderSystem
+import java.util.OptionalDouble
+import java.util.OptionalInt
+import util.render.CustomRenderPipelines
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.client.network.AbstractClientPlayerEntity
+import net.minecraft.client.render.BufferBuilder
+import net.minecraft.client.render.RenderLayer
+import net.minecraft.client.render.VertexConsumer
+import net.minecraft.client.render.VertexConsumerProvider
+import net.minecraft.client.render.entity.state.PlayerEntityRenderState
+import net.minecraft.client.util.BufferAllocator
+import net.minecraft.client.util.SkinTextures
+import net.minecraft.util.Identifier
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TimeMark
+
+object CustomCapes : FirmamentFeature {
+ override val identifier: String
+ get() = "developer-capes"
+
+ object TConfig : ManagedConfig(identifier, Category.DEV) {
+ val showCapes by toggle("show-cape") { true }
+ }
+
+ override val config: ManagedConfig
+ get() = TConfig
+
+ interface CustomCapeRenderer {
+ fun replaceRender(
+ renderLayer: RenderLayer,
+ vertexConsumerProvider: VertexConsumerProvider,
+ model: (VertexConsumer) -> Unit
+ )
+ }
+
+ data class TexturedCapeRenderer(
+ val location: Identifier
+ ) : CustomCapeRenderer {
+ override fun replaceRender(
+ renderLayer: RenderLayer,
+ vertexConsumerProvider: VertexConsumerProvider,
+ model: (VertexConsumer) -> Unit
+ ) {
+ model(vertexConsumerProvider.getBuffer(RenderLayer.getEntitySolid(location)))
+ }
+ }
+
+ data class ParallaxedHighlightCapeRenderer(
+ val template: Identifier,
+ val background: Identifier,
+ val overlay: Identifier,
+ val animationSpeed: Duration,
+ ) : CustomCapeRenderer {
+ override fun replaceRender(
+ renderLayer: RenderLayer,
+ vertexConsumerProvider: VertexConsumerProvider,
+ model: (VertexConsumer) -> Unit
+ ) {
+ BufferAllocator(2048).use { allocator ->
+ val bufferBuilder = BufferBuilder(allocator, renderLayer.drawMode, renderLayer.vertexFormat)
+ model(bufferBuilder)
+ bufferBuilder.end().use { buffer ->
+ val commandEncoder = RenderSystem.getDevice().createCommandEncoder()
+ val vertexBuffer = renderLayer.vertexFormat.uploadImmediateVertexBuffer(buffer.buffer)
+ val indexBufferConstructor = RenderSystem.getSequentialBuffer(renderLayer.drawMode)
+ val indexBuffer = indexBufferConstructor.getIndexBuffer(buffer.drawParameters.indexCount)
+ val templateTexture = MC.textureManager.getTexture(template)
+ val backgroundTexture = MC.textureManager.getTexture(background)
+ val foregroundTexture = MC.textureManager.getTexture(overlay)
+ commandEncoder.createRenderPass(
+ MC.instance.framebuffer.colorAttachment,
+ OptionalInt.empty(),
+ MC.instance.framebuffer.depthAttachment,
+ OptionalDouble.empty(),
+ ).use { renderPass ->
+ // TODO: account for lighting
+ renderPass.setPipeline(CustomRenderPipelines.PARALLAX_CAPE_SHADER)
+ renderPass.bindSampler("Sampler0", templateTexture.glTexture)
+ renderPass.bindSampler("Sampler1", backgroundTexture.glTexture)
+ renderPass.bindSampler("Sampler3", foregroundTexture.glTexture)
+ val animationValue = (startTime.passedTime() / animationSpeed).mod(1F)
+ renderPass.setUniform("Animation", animationValue.toFloat())
+ renderPass.setIndexBuffer(indexBuffer, indexBufferConstructor.indexType)
+ renderPass.setVertexBuffer(0, vertexBuffer)
+ renderPass.drawIndexed(0, buffer.drawParameters.indexCount)
+ }
+ }
+ }
+ }
+ }
+
+ interface CapeStorage {
+ companion object {
+ @JvmStatic
+ fun cast(playerEntityRenderState: PlayerEntityRenderState) =
+ playerEntityRenderState as CapeStorage
+
+ }
+
+ var cape_firmament: CustomCape?
+ }
+
+ data class CustomCape(
+ val id: String,
+ val label: String,
+ val render: CustomCapeRenderer,
+ )
+
+ enum class AllCapes(val label: String, val render: CustomCapeRenderer) {
+ FIRMAMENT_ANIMATED(
+ "Animated Firmament", ParallaxedHighlightCapeRenderer(
+ Firmament.identifier("textures/cape/parallax_template.png"),
+ Firmament.identifier("textures/cape/parallax_background.png"),
+ Firmament.identifier("textures/cape/firmament_star.png"),
+ 110.seconds
+ )
+ ),
+
+ FURFSKY_STATIC(
+ "FurfSky",
+ TexturedCapeRenderer(Firmament.identifier("textures/cape/fsr_static.png"))
+ ),
+
+ FIRMAMENT_STATIC(
+ "Firmament",
+ TexturedCapeRenderer(Firmament.identifier("textures/cape/firm_static.png"))
+ )
+ ;
+
+ val cape = CustomCape(name, label, render)
+ }
+
+ val byId = AllCapes.entries.associateBy { it.cape.id }
+ val byUuid =
+ listOf(
+ listOf(
+ Devs.nea to AllCapes.FIRMAMENT_ANIMATED,
+ Devs.kath to AllCapes.FIRMAMENT_STATIC,
+ Devs.jani to AllCapes.FIRMAMENT_ANIMATED,
+ ),
+ Devs.FurfSky.all.map { it to AllCapes.FURFSKY_STATIC },
+ ).flatten().flatMap { (dev, cape) -> dev.uuids.map { it to cape.cape } }.toMap()
+
+ @JvmStatic
+ fun render(
+ playerEntityRenderState: PlayerEntityRenderState,
+ vertexConsumer: VertexConsumer,
+ renderLayer: RenderLayer,
+ vertexConsumerProvider: VertexConsumerProvider,
+ model: (VertexConsumer) -> Unit
+ ) {
+ val capeStorage = CapeStorage.cast(playerEntityRenderState)
+ val firmCape = capeStorage.cape_firmament
+ if (firmCape != null) {
+ firmCape.render.replaceRender(renderLayer, vertexConsumerProvider, model)
+ } else {
+ model(vertexConsumer)
+ }
+ }
+
+ @JvmStatic
+ fun addCapeData(
+ player: AbstractClientPlayerEntity,
+ playerEntityRenderState: PlayerEntityRenderState
+ ) {
+ val cape = if (TConfig.showCapes) byUuid[player.uuid] else null
+ val capeStorage = CapeStorage.cast(playerEntityRenderState)
+ if (cape == null) {
+ capeStorage.cape_firmament = null
+ } else {
+ capeStorage.cape_firmament = cape
+ playerEntityRenderState.skinTextures = SkinTextures(
+ playerEntityRenderState.skinTextures.texture,
+ playerEntityRenderState.skinTextures.textureUrl,
+ Firmament.identifier("placeholder/fake_cape"),
+ playerEntityRenderState.skinTextures.elytraTexture,
+ playerEntityRenderState.skinTextures.model,
+ playerEntityRenderState.skinTextures.secure,
+ )
+ playerEntityRenderState.capeVisible = true
+ }
+ }
+
+ val startTime = TimeMark.now()
+}
diff --git a/src/main/kotlin/features/misc/Devs.kt b/src/main/kotlin/features/misc/Devs.kt
new file mode 100644
index 0000000..1f16400
--- /dev/null
+++ b/src/main/kotlin/features/misc/Devs.kt
@@ -0,0 +1,38 @@
+package moe.nea.firmament.features.misc
+
+import java.util.UUID
+
+object Devs {
+ data class Dev(
+ val uuids: List<UUID>,
+ ) {
+ constructor(vararg uuid: UUID) : this(uuid.toList())
+ constructor(vararg uuid: String) : this(uuid.map { UUID.fromString(it) })
+ }
+
+ val nea = Dev("d3cb85e2-3075-48a1-b213-a9bfb62360c1", "842204e6-6880-487b-ae5a-0595394f9948")
+ val kath = Dev("add71246-c46e-455c-8345-c129ea6f146c", "b491990d-53fd-4c5f-a61e-19d58cc7eddf")
+ val jani = Dev("8a9f1841-48e9-48ed-b14f-76a124e6c9df")
+
+ object FurfSky {
+ val smolegit = Dev("02b38b96-eb19-405a-b319-d6bc00b26ab3")
+ val itsCen = Dev("ada70b5a-ac37-49d2-b18c-1351672f8051")
+ val webster = Dev("02166f1b-9e8d-4e48-9e18-ea7a4499492d")
+ val vrachel = Dev("22e98637-ba97-4b6b-a84f-fb57a461ce43")
+ val cunuduh = Dev("2a15e3b3-c46e-4718-b907-166e1ab2efdc")
+ val eiiies = Dev("2ae162f2-81a7-4f91-a4b2-104e78a0a7e1")
+ val june = Dev("2584a4e3-f917-4493-8ced-618391f3b44f")
+ val denasu = Dev("313cbd25-8ade-4e41-845c-5cab555a30c9")
+ val libyKiwii = Dev("4265c52e-bd6f-4d3c-9cf6-bdfc8fb58023")
+ val madeleaan = Dev("bcb119a3-6000-4324-bda1-744f00c44b31")
+ val turtleSP = Dev("f1ca1934-a582-4723-8283-89921d008657")
+ val papayamm = Dev("ae0eea30-f6a2-40fe-ac17-9c80b3423409")
+ val persuasiveViksy = Dev("ba7ac144-28e0-4f55-a108-1a72fe744c9e")
+ val all = listOf(
+ smolegit, itsCen, webster, vrachel, cunuduh, eiiies,
+ june, denasu, libyKiwii, madeleaan, turtleSP, papayamm,
+ persuasiveViksy
+ )
+ }
+
+}
diff --git a/src/main/kotlin/features/misc/Hud.kt b/src/main/kotlin/features/misc/Hud.kt
new file mode 100644
index 0000000..9661fc5
--- /dev/null
+++ b/src/main/kotlin/features/misc/Hud.kt
@@ -0,0 +1,77 @@
+package moe.nea.firmament.features.misc
+
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HudRenderEvent
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.tr
+import moe.nea.jarvis.api.Point
+import net.minecraft.client.network.PlayerListEntry
+import net.minecraft.text.Text
+
+object Hud : FirmamentFeature {
+ override val identifier: String
+ get() = "hud"
+
+ object TConfig : ManagedConfig(identifier, Category.MISC) {
+ var dayCount by toggle("day-count") { false }
+ val dayCountHud by position("day-count-hud", 80, 10) { Point(0.5, 0.8) }
+ var fpsCount by toggle("fps-count") { false }
+ val fpsCountHud by position("fps-count-hud", 80, 10) { Point(0.5, 0.9) }
+ var pingCount by toggle("ping-count") { false }
+ val pingCountHud by position("ping-count-hud", 80, 10) { Point(0.5, 1.0) }
+ }
+
+ override val config: ManagedConfig
+ get() = TConfig
+
+ @Subscribe
+ fun onRenderHud(it: HudRenderEvent) {
+ if (TConfig.dayCount) {
+ it.context.matrices.push()
+ TConfig.dayCountHud.applyTransformations(it.context.matrices)
+ val day = (MC.world?.timeOfDay ?: 0L) / 24000
+ it.context.drawText(
+ MC.font,
+ Text.literal(String.format(tr("firmament.config.hud.day-count-hud.display", "Day: %s").string, day)),
+ 36,
+ MC.font.fontHeight,
+ -1,
+ true
+ )
+ it.context.matrices.pop()
+ }
+
+ if (TConfig.fpsCount) {
+ it.context.matrices.push()
+ TConfig.fpsCountHud.applyTransformations(it.context.matrices)
+ it.context.drawText(
+ MC.font, Text.literal(
+ String.format(
+ tr("firmament.config.hud.fps-count-hud.display", "FPS: %s").string, MC.instance.currentFps
+ )
+ ), 36, MC.font.fontHeight, -1, true
+ )
+ it.context.matrices.pop()
+ }
+
+ if (TConfig.pingCount) {
+ it.context.matrices.push()
+ TConfig.pingCountHud.applyTransformations(it.context.matrices)
+ val ping = MC.player?.let {
+ val entry: PlayerListEntry? = MC.networkHandler?.getPlayerListEntry(it.uuid)
+ entry?.latency ?: -1
+ } ?: -1
+ it.context.drawText(
+ MC.font, Text.literal(
+ String.format(
+ tr("firmament.config.hud.ping-count-hud.display", "Ping: %s ms").string, ping
+ )
+ ), 36, MC.font.fontHeight, -1, true
+ )
+
+ it.context.matrices.pop()
+ }
+ }
+}
diff --git a/src/main/kotlin/features/misc/LicenseViewer.kt b/src/main/kotlin/features/misc/LicenseViewer.kt
new file mode 100644
index 0000000..4219177
--- /dev/null
+++ b/src/main/kotlin/features/misc/LicenseViewer.kt
@@ -0,0 +1,128 @@
+package moe.nea.firmament.features.misc
+
+import io.github.notenoughupdates.moulconfig.observer.ObservableList
+import io.github.notenoughupdates.moulconfig.xml.Bind
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import kotlinx.serialization.json.decodeFromStream
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.MoulConfigUtils
+import moe.nea.firmament.util.ScreenUtil
+import moe.nea.firmament.util.tr
+
+object LicenseViewer {
+ @Serializable
+ data class Software(
+ val licenses: List<License> = listOf(),
+ val webPresence: String? = null,
+ val projectName: String,
+ val projectDescription: String? = null,
+ val developers: List<Developer> = listOf(),
+ ) {
+
+ @Bind
+ fun hasWebPresence() = webPresence != null
+
+ @Bind
+ fun webPresence() = webPresence ?: "<no web presence>"
+ @Bind
+ fun open() {
+ MC.openUrl(webPresence ?: return)
+ }
+
+ @Bind
+ fun projectName() = projectName
+
+ @Bind
+ fun projectDescription() = projectDescription ?: "<no project description>"
+
+ @get:Bind("developers")
+ @Transient
+ val developersO = ObservableList(developers)
+
+ @get:Bind("licenses")
+ @Transient
+ val licenses0 = ObservableList(licenses)
+ }
+
+ @Serializable
+ data class Developer(
+ @get:Bind("name") val name: String,
+ val webPresence: String? = null
+ ) {
+
+ @Bind
+ fun open() {
+ MC.openUrl(webPresence ?: return)
+ }
+
+ @Bind
+ fun hasWebPresence() = webPresence != null
+
+ @Bind
+ fun webPresence() = webPresence ?: "<no web presence>"
+ }
+
+ @Serializable
+ data class License(
+ @get:Bind("name") val licenseName: String,
+ val licenseUrl: String? = null
+ ) {
+ @Bind
+ fun open() {
+ MC.openUrl(licenseUrl ?: return)
+ }
+
+ @Bind
+ fun hasUrl() = licenseUrl != null
+
+ @Bind
+ fun url() = licenseUrl ?: "<no link to license text>"
+ }
+
+ data class LicenseList(
+ val softwares: List<Software>
+ ) {
+ @get:Bind("softwares")
+ val obs = ObservableList(softwares)
+ }
+
+ @OptIn(ExperimentalSerializationApi::class)
+ val licenses: LicenseList? = ErrorUtil.catch("Could not load licenses") {
+ Firmament.json.decodeFromStream<List<Software>?>(
+ javaClass.getResourceAsStream("/LICENSES-FIRMAMENT.json") ?: error("Could not find LICENSES-FIRMAMENT.json")
+ )?.let { LicenseList(it) }
+ }.orNull()
+
+ fun showLicenses() {
+ ErrorUtil.catch("Could not display licenses") {
+ ScreenUtil.setScreenLater(
+ MoulConfigUtils.loadScreen(
+ "license_viewer/index", licenses!!, null
+ )
+ )
+ }.or {
+ MC.sendChat(
+ tr(
+ "firmament.licenses.notfound",
+ "Could not load licenses. Please check the Firmament source code for information directly."
+ )
+ )
+ }
+ }
+
+ @Subscribe
+ fun onSubcommand(event: CommandEvent.SubCommand) {
+ event.subcommand("licenses") {
+ thenExecute {
+ showLicenses()
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/features/world/ColeWeightCompat.kt b/src/main/kotlin/features/world/ColeWeightCompat.kt
new file mode 100644
index 0000000..f7f1317
--- /dev/null
+++ b/src/main/kotlin/features/world/ColeWeightCompat.kt
@@ -0,0 +1,125 @@
+package moe.nea.firmament.features.world
+
+import kotlinx.serialization.Serializable
+import net.minecraft.text.Text
+import net.minecraft.util.math.BlockPos
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.DefaultSource
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.util.ClipboardUtils
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.tr
+
+object ColeWeightCompat {
+ @Serializable
+ data class ColeWeightWaypoint(
+ val x: Int?,
+ val y: Int?,
+ val z: Int?,
+ val r: Int = 0,
+ val g: Int = 0,
+ val b: Int = 0,
+ )
+
+ fun fromFirm(waypoints: FirmWaypoints, relativeTo: BlockPos): List<ColeWeightWaypoint> {
+ return waypoints.waypoints.map {
+ ColeWeightWaypoint(it.x - relativeTo.x, it.y - relativeTo.y, it.z - relativeTo.z)
+ }
+ }
+
+ fun intoFirm(waypoints: List<ColeWeightWaypoint>, relativeTo: BlockPos): FirmWaypoints {
+ val w = waypoints
+ .filter { it.x != null && it.y != null && it.z != null }
+ .map { FirmWaypoints.Waypoint(it.x!! + relativeTo.x, it.y!! + relativeTo.y, it.z!! + relativeTo.z) }
+ return FirmWaypoints(
+ "Imported Waypoints",
+ "imported",
+ null,
+ w.toMutableList(),
+ false
+ )
+ }
+
+ fun copyAndInform(
+ source: DefaultSource,
+ origin: BlockPos,
+ positiveFeedback: (Int) -> Text,
+ ) {
+ val waypoints = Waypoints.useNonEmptyWaypoints()
+ ?.let { fromFirm(it, origin) }
+ if (waypoints == null) {
+ source.sendError(Waypoints.textNothingToExport())
+ return
+ }
+ val data =
+ Firmament.tightJson.encodeToString<List<ColeWeightWaypoint>>(waypoints)
+ ClipboardUtils.setTextContent(data)
+ source.sendFeedback(positiveFeedback(waypoints.size))
+ }
+
+ fun importAndInform(
+ source: DefaultSource,
+ pos: BlockPos?,
+ positiveFeedback: (Int) -> Text
+ ) {
+ val text = ClipboardUtils.getTextContents()
+ val wr = tryParse(text).map { intoFirm(it, pos ?: BlockPos.ORIGIN) }
+ val waypoints = wr.getOrElse {
+ source.sendError(
+ tr("firmament.command.waypoint.import.cw.error",
+ "Could not import ColeWeight waypoints."))
+ Firmament.logger.error(it)
+ return
+ }
+ waypoints.lastRelativeImport = pos
+ Waypoints.waypoints = waypoints
+ source.sendFeedback(positiveFeedback(waypoints.size))
+ }
+
+ @Subscribe
+ fun onEvent(event: CommandEvent.SubCommand) {
+ event.subcommand(Waypoints.WAYPOINTS_SUBCOMMAND) {
+ thenLiteral("exportcw") {
+ thenExecute {
+ copyAndInform(source, BlockPos.ORIGIN) {
+ tr("firmament.command.waypoint.export.cw",
+ "Copied $it waypoints to clipboard in ColeWeight format.")
+ }
+ }
+ }
+ thenLiteral("exportrelativecw") {
+ thenExecute {
+ copyAndInform(source, MC.player?.blockPos ?: BlockPos.ORIGIN) {
+ tr("firmament.command.waypoint.export.cw.relative",
+ "Copied $it relative waypoints to clipboard in ColeWeight format. Make sure to stand in the same position when importing.")
+ }
+ }
+ }
+ thenLiteral("importcw") {
+ thenExecute {
+ importAndInform(source, null) {
+ tr("firmament.command.waypoint.import.cw.success",
+ "Imported $it waypoints from ColeWeight.")
+ }
+ }
+ }
+ thenLiteral("importrelativecw") {
+ thenExecute {
+ importAndInform(source, MC.player!!.blockPos) {
+ tr("firmament.command.waypoint.import.cw.relative",
+ "Imported $it relative waypoints from clipboard. Make sure you stand in the same position as when you exported these waypoints for them to line up correctly.")
+ }
+ }
+ }
+ }
+ }
+
+ fun tryParse(string: String): Result<List<ColeWeightWaypoint>> {
+ return runCatching {
+ Firmament.tightJson.decodeFromString<List<ColeWeightWaypoint>>(string)
+ }
+ }
+}
diff --git a/src/main/kotlin/features/world/FirmWaypointManager.kt b/src/main/kotlin/features/world/FirmWaypointManager.kt
new file mode 100644
index 0000000..d18483c
--- /dev/null
+++ b/src/main/kotlin/features/world/FirmWaypointManager.kt
@@ -0,0 +1,168 @@
+package moe.nea.firmament.features.world
+
+import com.mojang.brigadier.arguments.StringArgumentType
+import kotlinx.serialization.serializer
+import net.minecraft.text.Text
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.DefaultSource
+import moe.nea.firmament.commands.RestArgumentType
+import moe.nea.firmament.commands.get
+import moe.nea.firmament.commands.suggestsList
+import moe.nea.firmament.commands.thenArgument
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.util.ClipboardUtils
+import moe.nea.firmament.util.FirmFormatters
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TemplateUtil
+import moe.nea.firmament.util.data.MultiFileDataHolder
+import moe.nea.firmament.util.tr
+
+object FirmWaypointManager {
+ object DataHolder : MultiFileDataHolder<FirmWaypoints>(serializer(), "waypoints")
+
+ val SHARE_PREFIX = "FIRM_WAYPOINTS/"
+ val ENCODED_SHARE_PREFIX = TemplateUtil.getPrefixComparisonSafeBase64Encoding(SHARE_PREFIX)
+
+ fun createExportableCopy(
+ waypoints: FirmWaypoints,
+ ): FirmWaypoints {
+ val copy = waypoints.copy(waypoints = waypoints.waypoints.toMutableList())
+ if (waypoints.isRelativeTo != null) {
+ val origin = waypoints.lastRelativeImport
+ if (origin != null) {
+ copy.waypoints.replaceAll {
+ it.copy(
+ x = it.x - origin.x,
+ y = it.y - origin.y,
+ z = it.z - origin.z,
+ )
+ }
+ } else {
+ TODO("Add warning!")
+ }
+ }
+ return copy
+ }
+
+ fun loadWaypoints(waypoints: FirmWaypoints, sendFeedback: (Text) -> Unit) {
+ val copy = waypoints.deepCopy()
+ if (copy.isRelativeTo != null) {
+ val origin = MC.player!!.blockPos
+ copy.waypoints.replaceAll {
+ it.copy(
+ x = it.x + origin.x,
+ y = it.y + origin.y,
+ z = it.z + origin.z,
+ )
+ }
+ copy.lastRelativeImport = origin.toImmutable()
+ sendFeedback(tr("firmament.command.waypoint.import.ordered.success",
+ "Imported ${copy.size} relative waypoints. Make sure you stand in the correct spot while loading the waypoints: ${copy.isRelativeTo}."))
+ } else {
+ sendFeedback(tr("firmament.command.waypoint.import.success",
+ "Imported ${copy.size} waypoints."))
+ }
+ Waypoints.waypoints = copy
+ }
+
+ fun setOrigin(source: DefaultSource, text: String?) {
+ val waypoints = Waypoints.useEditableWaypoints()
+ waypoints.isRelativeTo = text ?: waypoints.isRelativeTo ?: ""
+ val pos = MC.player!!.blockPos
+ waypoints.lastRelativeImport = pos
+ source.sendFeedback(tr("firmament.command.waypoint.originset",
+ "Set the origin of waypoints to ${FirmFormatters.formatPosition(pos)}. Run /firm waypoints export to save the waypoints relative to this position."))
+ }
+
+ @Subscribe
+ fun onCommands(event: CommandEvent.SubCommand) {
+ event.subcommand(Waypoints.WAYPOINTS_SUBCOMMAND) {
+ thenLiteral("setorigin") {
+ thenExecute {
+ setOrigin(source, null)
+ }
+ thenArgument("hint", RestArgumentType) { text ->
+ thenExecute {
+ setOrigin(source, this[text])
+ }
+ }
+ }
+ thenLiteral("clearorigin") {
+ thenExecute {
+ val waypoints = Waypoints.useEditableWaypoints()
+ waypoints.lastRelativeImport = null
+ waypoints.isRelativeTo = null
+ source.sendFeedback(tr("firmament.command.waypoint.originunset",
+ "Unset the origin of the waypoints. Run /firm waypoints export to save the waypoints with absolute coordinates."))
+ }
+ }
+ thenLiteral("save") {
+ thenArgument("name", StringArgumentType.string()) { name ->
+ suggestsList { DataHolder.list().keys }
+ thenExecute {
+ val waypoints = Waypoints.useNonEmptyWaypoints()
+ if (waypoints == null) {
+ source.sendError(Waypoints.textNothingToExport())
+ return@thenExecute
+ }
+ waypoints.id = get(name)
+ val exportableWaypoints = createExportableCopy(waypoints)
+ DataHolder.insert(get(name), exportableWaypoints)
+ DataHolder.save()
+ source.sendFeedback(tr("firmament.command.waypoint.saved",
+ "Saved waypoints locally as ${get(name)}. Use /firm waypoints load to load them again."))
+ }
+ }
+ }
+ thenLiteral("load") {
+ thenArgument("name", StringArgumentType.string()) { name ->
+ suggestsList { DataHolder.list().keys }
+ thenExecute {
+ val name = get(name)
+ val waypoints = DataHolder.list()[name]
+ if (waypoints == null) {
+ source.sendError(
+ tr("firmament.command.waypoint.nosaved",
+ "No saved waypoint for ${name}. Use tab completion to see available names."))
+ return@thenExecute
+ }
+ loadWaypoints(waypoints, source::sendFeedback)
+ }
+ }
+ }
+ thenLiteral("export") {
+ thenExecute {
+ val waypoints = Waypoints.useNonEmptyWaypoints()
+ if (waypoints == null) {
+ source.sendError(Waypoints.textNothingToExport())
+ return@thenExecute
+ }
+ val exportableWaypoints = createExportableCopy(waypoints)
+ val data = TemplateUtil.encodeTemplate(SHARE_PREFIX, exportableWaypoints)
+ ClipboardUtils.setTextContent(data)
+ source.sendFeedback(tr("firmament.command.waypoint.export",
+ "Copied ${exportableWaypoints.size} waypoints to clipboard in Firmament format."))
+ }
+ }
+ thenLiteral("import") {
+ thenExecute {
+ val text = ClipboardUtils.getTextContents()
+ if (text.startsWith("[")) {
+ source.sendError(tr("firmament.command.waypoint.import.lookslikecw",
+ "The waypoints in your clipboard look like they might be ColeWeight waypoints. If so, use /firm waypoints importcw or /firm waypoints importrelativecw."))
+ return@thenExecute
+ }
+ val waypoints = TemplateUtil.maybeDecodeTemplate<FirmWaypoints>(SHARE_PREFIX, text)
+ if (waypoints == null) {
+ source.sendError(tr("firmament.command.waypoint.import.error",
+ "Could not import Firmament waypoints from your clipboard. Make sure they are Firmament compatible waypoints."))
+ return@thenExecute
+ }
+ loadWaypoints(waypoints, source::sendFeedback)
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/features/world/FirmWaypoints.kt b/src/main/kotlin/features/world/FirmWaypoints.kt
new file mode 100644
index 0000000..d0cd55a
--- /dev/null
+++ b/src/main/kotlin/features/world/FirmWaypoints.kt
@@ -0,0 +1,37 @@
+package moe.nea.firmament.features.world
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import net.minecraft.util.math.BlockPos
+
+@Serializable
+data class FirmWaypoints(
+ var label: String,
+ var id: String,
+ /**
+ * A hint to indicate where to stand while loading the waypoints.
+ */
+ var isRelativeTo: String?,
+ var waypoints: MutableList<Waypoint>,
+ var isOrdered: Boolean,
+ // TODO: val resetOnSwap: Boolean,
+) {
+
+ fun deepCopy() = copy(waypoints = waypoints.toMutableList())
+ @Transient
+ var lastRelativeImport: BlockPos? = null
+
+ val size get() = waypoints.size
+ @Serializable
+ data class Waypoint(
+ val x: Int,
+ val y: Int,
+ val z: Int,
+ ) {
+ val blockPos get() = BlockPos(x, y, z)
+
+ companion object {
+ fun from(blockPos: BlockPos) = Waypoint(blockPos.x, blockPos.y, blockPos.z)
+ }
+ }
+}
diff --git a/src/main/kotlin/features/world/TemporaryWaypoints.kt b/src/main/kotlin/features/world/TemporaryWaypoints.kt
new file mode 100644
index 0000000..b36c49d
--- /dev/null
+++ b/src/main/kotlin/features/world/TemporaryWaypoints.kt
@@ -0,0 +1,73 @@
+package moe.nea.firmament.features.world
+
+import kotlin.compareTo
+import kotlin.text.clear
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.text.Text
+import net.minecraft.util.math.BlockPos
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.ProcessChatEvent
+import moe.nea.firmament.events.WorldReadyEvent
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.features.world.Waypoints.TConfig
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.render.RenderInWorldContext
+
+object TemporaryWaypoints {
+ data class TemporaryWaypoint(
+ val pos: BlockPos,
+ val postedAt: TimeMark,
+ )
+ val temporaryPlayerWaypointList = mutableMapOf<String, TemporaryWaypoint>()
+ val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern()
+ @Subscribe
+ fun onProcessChat(it: ProcessChatEvent) {
+ val matcher = temporaryPlayerWaypointMatcher.matcher(it.unformattedString)
+ if (it.nameHeuristic != null && TConfig.tempWaypointDuration > 0.seconds && matcher.find()) {
+ temporaryPlayerWaypointList[it.nameHeuristic] = TemporaryWaypoint(BlockPos(
+ matcher.group(1).toInt(),
+ matcher.group(2).toInt(),
+ matcher.group(3).toInt(),
+ ), TimeMark.now())
+ }
+ }
+ @Subscribe
+ fun onRenderTemporaryWaypoints(event: WorldRenderLastEvent) {
+ temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration }
+ if (temporaryPlayerWaypointList.isEmpty()) return
+ RenderInWorldContext.renderInWorld(event) {
+ temporaryPlayerWaypointList.forEach { (_, waypoint) ->
+ block(waypoint.pos, 0xFFFFFF00.toInt())
+ }
+ temporaryPlayerWaypointList.forEach { (player, waypoint) ->
+ val skin =
+ MC.networkHandler?.listedPlayerListEntries?.find { it.profile.name == player }?.skinTextures?.texture
+ withFacingThePlayer(waypoint.pos.toCenterPos()) {
+ waypoint(waypoint.pos, Text.stringifiedTranslatable("firmament.waypoint.temporary", player))
+ if (skin != null) {
+ matrixStack.translate(0F, -20F, 0F)
+ // Head front
+ texture(
+ skin, 16, 16,
+ 1 / 8f, 1 / 8f,
+ 2 / 8f, 2 / 8f,
+ )
+ // Head overlay
+ texture(
+ skin, 16, 16,
+ 5 / 8f, 1 / 8f,
+ 6 / 8f, 2 / 8f,
+ )
+ }
+ }
+ }
+ }
+ }
+
+ @Subscribe
+ fun onWorldReady(event: WorldReadyEvent) {
+ temporaryPlayerWaypointList.clear()
+ }
+
+}
diff --git a/src/main/kotlin/features/world/Waypoints.kt b/src/main/kotlin/features/world/Waypoints.kt
index 2e4cb70..b5c2b66 100644
--- a/src/main/kotlin/features/world/Waypoints.kt
+++ b/src/main/kotlin/features/world/Waypoints.kt
@@ -2,36 +2,24 @@ package moe.nea.firmament.features.world
import com.mojang.brigadier.arguments.IntegerArgumentType
import me.shedaniel.math.Color
-import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.encodeToString
-import kotlin.collections.component1
-import kotlin.collections.component2
-import kotlin.collections.set
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
import net.minecraft.command.argument.BlockPosArgumentType
-import net.minecraft.server.command.CommandOutput
-import net.minecraft.server.command.ServerCommandSource
import net.minecraft.text.Text
-import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Vec3d
-import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.get
import moe.nea.firmament.commands.thenArgument
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.commands.thenLiteral
import moe.nea.firmament.events.CommandEvent
-import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.events.WorldReadyEvent
import moe.nea.firmament.events.WorldRenderLastEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
-import moe.nea.firmament.util.ClipboardUtils
import moe.nea.firmament.util.MC
-import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.mc.asFakeServer
import moe.nea.firmament.util.render.RenderInWorldContext
import moe.nea.firmament.util.tr
@@ -43,99 +31,85 @@ object Waypoints : FirmamentFeature {
val tempWaypointDuration by duration("temp-waypoint-duration", 0.seconds, 1.hours) { 30.seconds }
val showIndex by toggle("show-index") { true }
val skipToNearest by toggle("skip-to-nearest") { false }
+ val resetWaypointOrderOnWorldSwap by toggle("reset-order-on-swap") { true }
// TODO: look ahead size
}
- data class TemporaryWaypoint(
- val pos: BlockPos,
- val postedAt: TimeMark,
- )
-
override val config get() = TConfig
-
- val temporaryPlayerWaypointList = mutableMapOf<String, TemporaryWaypoint>()
- val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern()
-
- val waypoints = mutableListOf<BlockPos>()
- var ordered = false
+ var waypoints: FirmWaypoints? = null
var orderedIndex = 0
- @Serializable
- data class ColeWeightWaypoint(
- val x: Int,
- val y: Int,
- val z: Int,
- val r: Int = 0,
- val g: Int = 0,
- val b: Int = 0,
- )
-
@Subscribe
fun onRenderOrderedWaypoints(event: WorldRenderLastEvent) {
- if (waypoints.isEmpty()) return
+ val w = useNonEmptyWaypoints() ?: return
RenderInWorldContext.renderInWorld(event) {
- if (!ordered) {
- waypoints.withIndex().forEach {
- block(it.value, 0x800050A0.toInt())
- if (TConfig.showIndex)
- withFacingThePlayer(it.value.toCenterPos()) {
- text(Text.literal(it.index.toString()))
- }
+ if (!w.isOrdered) {
+ w.waypoints.withIndex().forEach {
+ block(it.value.blockPos, 0x800050A0.toInt())
+ if (TConfig.showIndex) withFacingThePlayer(it.value.blockPos.toCenterPos()) {
+ text(Text.literal(it.index.toString()))
+ }
}
} else {
- orderedIndex %= waypoints.size
+ orderedIndex %= w.waypoints.size
val firstColor = Color.ofRGBA(0, 200, 40, 180)
color(firstColor)
- tracer(waypoints[orderedIndex].toCenterPos(), lineWidth = 3f)
- waypoints.withIndex().toList()
- .wrappingWindow(orderedIndex, 3)
- .zip(
- listOf(
- firstColor,
- Color.ofRGBA(180, 200, 40, 150),
- Color.ofRGBA(180, 80, 20, 140),
- )
- )
- .reversed()
- .forEach { (waypoint, col) ->
- val (index, pos) = waypoint
- block(pos, col.color)
- if (TConfig.showIndex)
- withFacingThePlayer(pos.toCenterPos()) {
- text(Text.literal(index.toString()))
- }
+ tracer(w.waypoints[orderedIndex].blockPos.toCenterPos(), lineWidth = 3f)
+ w.waypoints.withIndex().toList().wrappingWindow(orderedIndex, 3).zip(listOf(
+ firstColor,
+ Color.ofRGBA(180, 200, 40, 150),
+ Color.ofRGBA(180, 80, 20, 140),
+ )).reversed().forEach { (waypoint, col) ->
+ val (index, pos) = waypoint
+ block(pos.blockPos, col.color)
+ if (TConfig.showIndex) withFacingThePlayer(pos.blockPos.toCenterPos()) {
+ text(Text.literal(index.toString()))
}
+ }
}
}
}
@Subscribe
fun onTick(event: TickEvent) {
- if (waypoints.isEmpty() || !ordered) return
- orderedIndex %= waypoints.size
+ val w = useNonEmptyWaypoints() ?: return
+ if (!w.isOrdered) return
+ orderedIndex %= w.waypoints.size
val p = MC.player?.pos ?: return
if (TConfig.skipToNearest) {
orderedIndex =
- (waypoints.withIndex().minBy { it.value.getSquaredDistance(p) }.index + 1) % waypoints.size
+ (w.waypoints.withIndex().minBy { it.value.blockPos.getSquaredDistance(p) }.index + 1) % w.waypoints.size
+
} else {
- if (waypoints[orderedIndex].isWithinDistance(p, 3.0)) {
- orderedIndex = (orderedIndex + 1) % waypoints.size
+ if (w.waypoints[orderedIndex].blockPos.isWithinDistance(p, 3.0)) {
+ orderedIndex = (orderedIndex + 1) % w.waypoints.size
}
}
}
+
+ fun useEditableWaypoints(): FirmWaypoints {
+ var w = waypoints
+ if (w == null) {
+ w = FirmWaypoints("Unlabeled", "unknown", null, mutableListOf(), false)
+ waypoints = w
+ }
+ return w
+ }
+
+ fun useNonEmptyWaypoints(): FirmWaypoints? {
+ val w = waypoints
+ if (w == null) return null
+ if (w.waypoints.isEmpty()) return null
+ return w
+ }
+
+ val WAYPOINTS_SUBCOMMAND = "waypoints"
+
@Subscribe
- fun onProcessChat(it: ProcessChatEvent) {
- val matcher = temporaryPlayerWaypointMatcher.matcher(it.unformattedString)
- if (it.nameHeuristic != null && TConfig.tempWaypointDuration > 0.seconds && matcher.find()) {
- temporaryPlayerWaypointList[it.nameHeuristic] = TemporaryWaypoint(
- BlockPos(
- matcher.group(1).toInt(),
- matcher.group(2).toInt(),
- matcher.group(3).toInt(),
- ),
- TimeMark.now()
- )
+ fun onWorldSwap(event: WorldReadyEvent) {
+ if (TConfig.resetWaypointOrderOnWorldSwap) {
+ orderedIndex = 0
}
}
@@ -144,41 +118,77 @@ object Waypoints : FirmamentFeature {
event.subcommand("waypoint") {
thenArgument("pos", BlockPosArgumentType.blockPos()) { pos ->
thenExecute {
+ source
val position = pos.get(this).toAbsoluteBlockPos(source.asFakeServer())
- waypoints.add(position)
- source.sendFeedback(
- Text.stringifiedTranslatable(
- "firmament.command.waypoint.added",
- position.x,
- position.y,
- position.z
- )
- )
+ val w = useEditableWaypoints()
+ w.waypoints.add(FirmWaypoints.Waypoint.from(position))
+ source.sendFeedback(Text.stringifiedTranslatable("firmament.command.waypoint.added",
+ position.x,
+ position.y,
+ position.z))
}
}
}
- event.subcommand("waypoints") {
+ event.subcommand(WAYPOINTS_SUBCOMMAND) {
+ thenLiteral("reset") {
+ thenExecute {
+ orderedIndex = 0
+ source.sendFeedback(tr(
+ "firmament.command.waypoint.reset",
+ "Reset your ordered waypoint index back to 0. If you want to delete all waypoints use /firm waypoints clear instead."))
+ }
+ }
+ thenLiteral("changeindex") {
+ thenArgument("from", IntegerArgumentType.integer(0)) { fromIndex ->
+ thenArgument("to", IntegerArgumentType.integer(0)) { toIndex ->
+ thenExecute {
+ val w = useEditableWaypoints()
+ val toIndex = toIndex.get(this)
+ val fromIndex = fromIndex.get(this)
+ if (fromIndex !in w.waypoints.indices) {
+ source.sendError(textInvalidIndex(fromIndex))
+ return@thenExecute
+ }
+ if (toIndex !in w.waypoints.indices) {
+ source.sendError(textInvalidIndex(toIndex))
+ return@thenExecute
+ }
+ val waypoint = w.waypoints.removeAt(fromIndex)
+ w.waypoints.add(
+ if (toIndex > fromIndex) toIndex - 1
+ else toIndex,
+ waypoint)
+ source.sendFeedback(
+ tr("firmament.command.waypoint.indexchange",
+ "Moved waypoint from index $fromIndex to $toIndex. Note that this only matters for ordered waypoints.")
+ )
+ }
+ }
+ }
+ }
thenLiteral("clear") {
thenExecute {
- waypoints.clear()
+ waypoints = null
source.sendFeedback(Text.translatable("firmament.command.waypoint.clear"))
}
}
thenLiteral("toggleordered") {
thenExecute {
- ordered = !ordered
- if (ordered) {
+ val w = useEditableWaypoints()
+ w.isOrdered = !w.isOrdered
+ if (w.isOrdered) {
val p = MC.player?.pos ?: Vec3d.ZERO
- orderedIndex =
- waypoints.withIndex().minByOrNull { it.value.getSquaredDistance(p) }?.index ?: 0
+ orderedIndex = // TODO: this should be extracted to a utility method
+ w.waypoints.withIndex().minByOrNull { it.value.blockPos.getSquaredDistance(p) }?.index ?: 0
}
- source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.$ordered"))
+ source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.${w.isOrdered}"))
}
}
thenLiteral("skip") {
thenExecute {
- if (ordered && waypoints.isNotEmpty()) {
- orderedIndex = (orderedIndex + 1) % waypoints.size
+ val w = useNonEmptyWaypoints()
+ if (w != null && w.isOrdered) {
+ orderedIndex = (orderedIndex + 1) % w.size
source.sendFeedback(Text.translatable("firmament.command.waypoint.skip"))
} else {
source.sendError(Text.translatable("firmament.command.waypoint.skip.error"))
@@ -189,118 +199,27 @@ object Waypoints : FirmamentFeature {
thenArgument("index", IntegerArgumentType.integer(0)) { indexArg ->
thenExecute {
val index = get(indexArg)
- if (index in waypoints.indices) {
- waypoints.removeAt(index)
- source.sendFeedback(Text.stringifiedTranslatable(
- "firmament.command.waypoint.remove",
- index))
+ val w = useNonEmptyWaypoints()
+ if (w != null && index in w.waypoints.indices) {
+ w.waypoints.removeAt(index)
+ source.sendFeedback(Text.stringifiedTranslatable("firmament.command.waypoint.remove",
+ index))
} else {
source.sendError(Text.stringifiedTranslatable("firmament.command.waypoint.remove.error"))
}
}
}
}
- thenLiteral("export") {
- thenExecute {
- val data = Firmament.tightJson.encodeToString<List<ColeWeightWaypoint>>(waypoints.map {
- ColeWeightWaypoint(it.x,
- it.y,
- it.z)
- })
- ClipboardUtils.setTextContent(data)
- source.sendFeedback(tr("firmament.command.waypoint.export",
- "Copied ${waypoints.size} waypoints to clipboard"))
- }
- }
- thenLiteral("exportrelative") {
- thenExecute {
- val playerPos = MC.player!!.blockPos
- val x = playerPos.x
- val y = playerPos.y
- val z = playerPos.z
- val data = Firmament.tightJson.encodeToString<List<ColeWeightWaypoint>>(waypoints.map {
- ColeWeightWaypoint(it.x - x,
- it.y - y,
- it.z - z)
- })
- ClipboardUtils.setTextContent(data)
- source.sendFeedback(tr("firmament.command.waypoint.export.relative",
- "Copied ${waypoints.size} relative waypoints to clipboard. Make sure to stand in the same position when importing."))
-
- }
- }
- thenLiteral("import") {
- thenExecute {
- source.sendFeedback(
- importRelative(BlockPos.ORIGIN)
- ?: Text.stringifiedTranslatable("firmament.command.waypoint.import", waypoints.size),
- )
- }
- }
- thenLiteral("importrelative") {
- thenExecute {
- source.sendFeedback(
- importRelative(MC.player!!.blockPos)
- ?: tr("firmament.command.waypoint.import.relative",
- "Imported ${waypoints.size} relative waypoints from clipboard. Make sure you stand in the same position as when you exported these waypoints for them to line up correctly."),
- )
- }
- }
}
}
- fun importRelative(pos: BlockPos): Text? {
- val contents = ClipboardUtils.getTextContents()
- val data = try {
- Firmament.tightJson.decodeFromString<List<ColeWeightWaypoint>>(contents)
- } catch (ex: Exception) {
- Firmament.logger.error("Could not load waypoints from clipboard", ex)
- return (Text.translatable("firmament.command.waypoint.import.error"))
- }
- waypoints.clear()
- data.mapTo(waypoints) { BlockPos(it.x + pos.x, it.y + pos.y, it.z + pos.z) }
- return null
- }
-
- @Subscribe
- fun onRenderTemporaryWaypoints(event: WorldRenderLastEvent) {
- temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration }
- if (temporaryPlayerWaypointList.isEmpty()) return
- RenderInWorldContext.renderInWorld(event) {
- temporaryPlayerWaypointList.forEach { (player, waypoint) ->
- block(waypoint.pos, 0xFFFFFF00.toInt())
- }
- temporaryPlayerWaypointList.forEach { (player, waypoint) ->
- val skin =
- MC.networkHandler?.listedPlayerListEntries?.find { it.profile.name == player }
- ?.skinTextures
- ?.texture
- withFacingThePlayer(waypoint.pos.toCenterPos()) {
- waypoint(waypoint.pos, Text.stringifiedTranslatable("firmament.waypoint.temporary", player))
- if (skin != null) {
- matrixStack.translate(0F, -20F, 0F)
- // Head front
- texture(
- skin, 16, 16,
- 1 / 8f, 1 / 8f,
- 2 / 8f, 2 / 8f,
- )
- // Head overlay
- texture(
- skin, 16, 16,
- 5 / 8f, 1 / 8f,
- 6 / 8f, 2 / 8f,
- )
- }
- }
- }
- }
- }
+ fun textInvalidIndex(index: Int) =
+ tr("firmament.command.waypoint.invalid-index",
+ "Invalid index $index provided.")
- @Subscribe
- fun onWorldReady(event: WorldReadyEvent) {
- temporaryPlayerWaypointList.clear()
- }
+ fun textNothingToExport(): Text =
+ tr("firmament.command.waypoint.export.nowaypoints",
+ "No waypoints to export found. Add some with /firm waypoint ~ ~ ~.")
}
fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> {
@@ -313,35 +232,3 @@ fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> {
}
return result
}
-
-
-fun FabricClientCommandSource.asFakeServer(): ServerCommandSource {
- val source = this
- return ServerCommandSource(
- object : CommandOutput {
- override fun sendMessage(message: Text?) {
- source.player.sendMessage(message, false)
- }
-
- override fun shouldReceiveFeedback(): Boolean {
- return true
- }
-
- override fun shouldTrackOutput(): Boolean {
- return true
- }
-
- override fun shouldBroadcastConsoleToOps(): Boolean {
- return true
- }
- },
- source.position,
- source.rotation,
- null,
- 0,
- "FakeServerCommandSource",
- Text.literal("FakeServerCommandSource"),
- null,
- source.player
- )
-}
diff --git a/src/main/kotlin/gui/config/AllConfigsGui.kt b/src/main/kotlin/gui/config/AllConfigsGui.kt
index 73ff444..f9ffd2d 100644
--- a/src/main/kotlin/gui/config/AllConfigsGui.kt
+++ b/src/main/kotlin/gui/config/AllConfigsGui.kt
@@ -4,6 +4,12 @@ import io.github.notenoughupdates.moulconfig.observer.ObservableList
import io.github.notenoughupdates.moulconfig.xml.Bind
import net.minecraft.client.gui.screen.Screen
import net.minecraft.text.Text
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.RestArgumentType
+import moe.nea.firmament.commands.get
+import moe.nea.firmament.commands.thenArgument
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.MoulConfigUtils
import moe.nea.firmament.util.ScreenUtil.setScreenLater
@@ -18,6 +24,7 @@ object AllConfigsGui {
object ConfigConfig : ManagedConfig("configconfig", Category.META) {
val enableYacl by toggle("enable-yacl") { false }
val enableMoulConfig by toggle("enable-moulconfig") { true }
+ val enableWideMC by toggle("wide-moulconfig") { false }
}
fun <T> List<T>.toObservableList(): ObservableList<T> = ObservableList(this)
@@ -66,7 +73,7 @@ object AllConfigsGui {
return MoulConfigUtils.loadScreen("config/main", CategoryView(), parent)
}
- fun makeScreen(parent: Screen? = null): Screen {
+ fun makeScreen(search: String? = null, parent: Screen? = null): Screen {
val wantedKey = when {
ConfigConfig.enableMoulConfig -> "moulconfig"
ConfigConfig.enableYacl -> "yacl"
@@ -74,10 +81,23 @@ object AllConfigsGui {
}
val provider = FirmamentConfigScreenProvider.providers.find { it.key == wantedKey }
?: FirmamentConfigScreenProvider.providers.first()
- return provider.open(parent)
+ return provider.open(search, parent)
}
fun showAllGuis() {
setScreenLater(makeScreen())
}
+
+ @Subscribe
+ fun registerCommands(event: CommandEvent.SubCommand) {
+ event.subcommand("search") {
+ thenArgument("search", RestArgumentType) { search ->
+ thenExecute {
+ val search = this[search]
+ setScreenLater(makeScreen(search = search))
+ }
+ }
+ }
+ }
+
}
diff --git a/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt b/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt
index 19e7383..8ecdfa2 100644
--- a/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt
+++ b/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt
@@ -8,7 +8,7 @@ class BuiltInConfigScreenProvider : FirmamentConfigScreenProvider {
override val key: String
get() = "builtin"
- override fun open(parent: Screen?): Screen {
+ override fun open(search: String?, parent: Screen?): Screen {
return AllConfigsGui.makeBuiltInScreen(parent)
}
}
diff --git a/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt b/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt
index faad1cc..8700ffa 100644
--- a/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt
+++ b/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt
@@ -7,7 +7,7 @@ interface FirmamentConfigScreenProvider {
val key: String
val isEnabled: Boolean get() = true
- fun open(parent: Screen?): Screen
+ fun open(search: String?, parent: Screen?): Screen
companion object : CompatLoader<FirmamentConfigScreenProvider>(FirmamentConfigScreenProvider::class) {
val providers by lazy {
diff --git a/src/main/kotlin/gui/config/KeyBindingHandler.kt b/src/main/kotlin/gui/config/KeyBindingHandler.kt
index d7d0b47..14a4b32 100644
--- a/src/main/kotlin/gui/config/KeyBindingHandler.kt
+++ b/src/main/kotlin/gui/config/KeyBindingHandler.kt
@@ -40,34 +40,7 @@ class KeyBindingHandler(val name: String, val managedConfig: ManagedConfig) :
{ button.blur() },
{ button.requestFocus() }
)
- button = object : FirmButtonComponent(
- TextComponent(
- IMinecraft.instance.defaultFontRenderer,
- { sm.label.string },
- 130,
- TextComponent.TextAlignment.LEFT,
- false,
- false
- ), action = {
- sm.onClick()
- }) {
- override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean {
- if (event is KeyboardEvent.KeyPressed) {
- return sm.keyboardEvent(event.keycode, event.pressed)
- }
- return super.keyboardEvent(event, context)
- }
-
- override fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> {
- if (sm.editing) return activeBg
- return super.getBackground(context)
- }
-
-
- override fun onLostFocus() {
- sm.onLostFocus()
- }
- }
+ button = sm.createButton()
sm.updateLabel()
return button
}
diff --git a/src/main/kotlin/gui/config/KeyBindingStateManager.kt b/src/main/kotlin/gui/config/KeyBindingStateManager.kt
index cc8178d..1528ac4 100644
--- a/src/main/kotlin/gui/config/KeyBindingStateManager.kt
+++ b/src/main/kotlin/gui/config/KeyBindingStateManager.kt
@@ -1,8 +1,15 @@
package moe.nea.firmament.gui.config
+import io.github.notenoughupdates.moulconfig.common.IMinecraft
+import io.github.notenoughupdates.moulconfig.common.MyResourceLocation
+import io.github.notenoughupdates.moulconfig.deps.libninepatch.NinePatch
+import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
+import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent
+import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
import org.lwjgl.glfw.GLFW
import net.minecraft.text.Text
import net.minecraft.util.Formatting
+import moe.nea.firmament.gui.FirmButtonComponent
import moe.nea.firmament.keybindings.SavedKeyBinding
class KeyBindingStateManager(
@@ -51,9 +58,11 @@ class KeyBindingStateManager(
) {
lastPressed = ch
} else {
- setValue(SavedKeyBinding(
- ch, modifiers
- ))
+ setValue(
+ SavedKeyBinding(
+ ch, modifiers
+ )
+ )
editing = false
blur()
lastPressed = 0
@@ -104,5 +113,34 @@ class KeyBindingStateManager(
label = stroke
}
+ fun createButton(): FirmButtonComponent {
+ return object : FirmButtonComponent(
+ TextComponent(
+ IMinecraft.instance.defaultFontRenderer,
+ { this@KeyBindingStateManager.label.string },
+ 130,
+ TextComponent.TextAlignment.LEFT,
+ false,
+ false
+ ), action = {
+ this@KeyBindingStateManager.onClick()
+ }) {
+ override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean {
+ if (event is KeyboardEvent.KeyPressed) {
+ return this@KeyBindingStateManager.keyboardEvent(event.keycode, event.pressed)
+ }
+ return super.keyboardEvent(event, context)
+ }
+
+ override fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> {
+ if (this@KeyBindingStateManager.editing) return activeBg
+ return super.getBackground(context)
+ }
+
+ override fun onLostFocus() {
+ this@KeyBindingStateManager.onLostFocus()
+ }
+ }
+ }
}
diff --git a/src/main/kotlin/gui/config/ManagedConfig.kt b/src/main/kotlin/gui/config/ManagedConfig.kt
index 7ddda9e..ba6792d 100644
--- a/src/main/kotlin/gui/config/ManagedConfig.kt
+++ b/src/main/kotlin/gui/config/ManagedConfig.kt
@@ -38,7 +38,9 @@ abstract class ManagedConfig(
MISC,
CHAT,
INVENTORY,
+ ITEMS,
MINING,
+ GARDEN,
EVENTS,
INTEGRATIONS,
META,
@@ -69,6 +71,7 @@ abstract class ManagedConfig(
category.configs.add(this)
}
+ // TODO: warn if two files use the same config file name :(
val file = Firmament.CONFIG_DIR.resolve("$name.json")
val data: JsonObject by lazy {
try {
diff --git a/src/main/kotlin/gui/config/ManagedOption.kt b/src/main/kotlin/gui/config/ManagedOption.kt
index 383f392..830086c 100644
--- a/src/main/kotlin/gui/config/ManagedOption.kt
+++ b/src/main/kotlin/gui/config/ManagedOption.kt
@@ -49,7 +49,7 @@ class ManagedOption<T : Any>(
value = handler.fromJson(root[propertyName]!!)
return
} catch (e: Exception) {
- ErrorUtil.softError(
+ ErrorUtil.logError(
"Exception during loading of config file ${element.name}. This will reset this config.",
e
)
diff --git a/src/main/kotlin/gui/entity/EntityRenderer.kt b/src/main/kotlin/gui/entity/EntityRenderer.kt
index fd7a0c4..a1b2577 100644
--- a/src/main/kotlin/gui/entity/EntityRenderer.kt
+++ b/src/main/kotlin/gui/entity/EntityRenderer.kt
@@ -27,41 +27,79 @@ object EntityRenderer {
}
val entityIds: Map<String, () -> LivingEntity> = mapOf(
- "Zombie" to t(EntityType.ZOMBIE),
+ "Armadillo" to t(EntityType.ARMADILLO),
+ "ArmorStand" to t(EntityType.ARMOR_STAND),
+ "Axolotl" to t(EntityType.AXOLOTL),
+ "BREEZE" to t(EntityType.BREEZE),
+ "Bat" to t(EntityType.BAT),
+ "Bee" to t(EntityType.BEE),
+ "Blaze" to t(EntityType.BLAZE),
+ "CaveSpider" to t(EntityType.CAVE_SPIDER),
"Chicken" to t(EntityType.CHICKEN),
- "Slime" to t(EntityType.SLIME),
- "Wolf" to t(EntityType.WOLF),
- "Skeleton" to t(EntityType.SKELETON),
+ "Cod" to t(EntityType.COD),
+ "Cow" to t(EntityType.COW),
+ "Creaking" to t(EntityType.CREAKING),
"Creeper" to t(EntityType.CREEPER),
+ "Dolphin" to t(EntityType.DOLPHIN),
+ "Donkey" to t(EntityType.DONKEY),
+ "Dragon" to t(EntityType.ENDER_DRAGON),
+ "Drowned" to t(EntityType.DROWNED),
+ "Eisengolem" to t(EntityType.IRON_GOLEM),
+ "Enderman" to t(EntityType.ENDERMAN),
+ "Endermite" to t(EntityType.ENDERMITE),
+ "Evoker" to t(EntityType.EVOKER),
+ "Fox" to t(EntityType.FOX),
+ "Frog" to t(EntityType.FROG),
+ "Ghast" to t(EntityType.GHAST),
+ "Giant" to t(EntityType.GIANT),
+ "GlowSquid" to t(EntityType.GLOW_SQUID),
+ "Goat" to t(EntityType.GOAT),
+ "Guardian" to t(EntityType.GUARDIAN),
+ "Horse" to t(EntityType.HORSE),
+ "Husk" to t(EntityType.HUSK),
+ "Illusioner" to t(EntityType.ILLUSIONER),
+ "LLama" to t(EntityType.LLAMA),
+ "MagmaCube" to t(EntityType.MAGMA_CUBE),
+ "Mooshroom" to t(EntityType.MOOSHROOM),
+ "Mule" to t(EntityType.MULE),
"Ocelot" to t(EntityType.OCELOT),
- "Blaze" to t(EntityType.BLAZE),
+ "Panda" to t(EntityType.PANDA),
+ "Phantom" to t(EntityType.PHANTOM),
+ "Pig" to t(EntityType.PIG),
+ "Piglin" to t(EntityType.PIGLIN),
+ "PiglinBrute" to t(EntityType.PIGLIN_BRUTE),
+ "Pigman" to t(EntityType.ZOMBIFIED_PIGLIN),
+ "Pillager" to t(EntityType.PILLAGER),
+ "Player" to { makeGuiPlayer(fakeWorld) },
+ "PolarBear" to t(EntityType.POLAR_BEAR),
+ "Pufferfish" to t(EntityType.PUFFERFISH),
"Rabbit" to t(EntityType.RABBIT),
+ "Salmom" to t(EntityType.SALMON),
"Sheep" to t(EntityType.SHEEP),
- "Horse" to t(EntityType.HORSE),
- "Eisengolem" to t(EntityType.IRON_GOLEM),
+ "Shulker" to t(EntityType.SHULKER),
"Silverfish" to t(EntityType.SILVERFISH),
- "Witch" to t(EntityType.WITCH),
- "Endermite" to t(EntityType.ENDERMITE),
+ "Skeleton" to t(EntityType.SKELETON),
+ "Slime" to t(EntityType.SLIME),
+ "Sniffer" to t(EntityType.SNIFFER),
"Snowman" to t(EntityType.SNOW_GOLEM),
- "Villager" to t(EntityType.VILLAGER),
- "Guardian" to t(EntityType.GUARDIAN),
- "ArmorStand" to t(EntityType.ARMOR_STAND),
- "Squid" to t(EntityType.SQUID),
- "Bat" to t(EntityType.BAT),
"Spider" to t(EntityType.SPIDER),
- "CaveSpider" to t(EntityType.CAVE_SPIDER),
- "Pigman" to t(EntityType.ZOMBIFIED_PIGLIN),
- "Ghast" to t(EntityType.GHAST),
- "MagmaCube" to t(EntityType.MAGMA_CUBE),
+ "Squid" to t(EntityType.SQUID),
+ "Stray" to t(EntityType.STRAY),
+ "Strider" to t(EntityType.STRIDER),
+ "Tadpole" to t(EntityType.TADPOLE),
+ "TropicalFish" to t(EntityType.TROPICAL_FISH),
+ "Turtle" to t(EntityType.TURTLE),
+ "Vex" to t(EntityType.VEX),
+ "Villager" to t(EntityType.VILLAGER),
+ "Vindicator" to t(EntityType.VINDICATOR),
+ "Warden" to t(EntityType.WARDEN),
+ "Witch" to t(EntityType.WITCH),
"Wither" to t(EntityType.WITHER),
- "Enderman" to t(EntityType.ENDERMAN),
- "Mooshroom" to t(EntityType.MOOSHROOM),
"WitherSkeleton" to t(EntityType.WITHER_SKELETON),
- "Cow" to t(EntityType.COW),
- "Dragon" to t(EntityType.ENDER_DRAGON),
- "Player" to { makeGuiPlayer(fakeWorld) },
- "Pig" to t(EntityType.PIG),
- "Giant" to t(EntityType.GIANT),
+ "Wolf" to t(EntityType.WOLF),
+ "Zoglin" to t(EntityType.ZOGLIN),
+ "Zombie" to t(EntityType.ZOMBIE),
+ "ZombieVillager" to t(EntityType.ZOMBIE_VILLAGER)
)
val entityModifiers: Map<String, EntityModifier> = mapOf(
"playerdata" to ModifyPlayerSkin,
@@ -169,13 +207,13 @@ object EntityRenderer {
val oldBodyYaw = entity.bodyYaw
val oldYaw = entity.yaw
val oldPitch = entity.pitch
- val oldPrevHeadYaw = entity.prevHeadYaw
+ val oldPrevHeadYaw = entity.lastHeadYaw
val oldHeadYaw = entity.headYaw
entity.bodyYaw = 180.0f + targetYaw * 20.0f
entity.yaw = 180.0f + targetYaw * 40.0f
entity.pitch = -targetPitch * 20.0f
entity.headYaw = entity.yaw
- entity.prevHeadYaw = entity.yaw
+ entity.lastHeadYaw = entity.yaw
val vector3f = Vector3f(0.0f, (entity.height / 2.0f + bottomOffset).toFloat(), 0.0f)
InventoryScreen.drawEntity(
context,
@@ -190,7 +228,7 @@ object EntityRenderer {
entity.bodyYaw = oldBodyYaw
entity.yaw = oldYaw
entity.pitch = oldPitch
- entity.prevHeadYaw = oldPrevHeadYaw
+ entity.lastHeadYaw = oldPrevHeadYaw
entity.headYaw = oldHeadYaw
context.disableScissor()
}
diff --git a/src/main/kotlin/gui/entity/FakeWorld.kt b/src/main/kotlin/gui/entity/FakeWorld.kt
deleted file mode 100644
index ccf6b60..0000000
--- a/src/main/kotlin/gui/entity/FakeWorld.kt
+++ /dev/null
@@ -1,343 +0,0 @@
-package moe.nea.firmament.gui.entity
-
-import java.util.UUID
-import java.util.function.BooleanSupplier
-import java.util.function.Consumer
-import net.minecraft.block.Block
-import net.minecraft.block.BlockState
-import net.minecraft.client.gui.screen.world.SelectWorldScreen
-import net.minecraft.component.type.MapIdComponent
-import net.minecraft.entity.Entity
-import net.minecraft.entity.boss.dragon.EnderDragonPart
-import net.minecraft.entity.damage.DamageSource
-import net.minecraft.entity.player.PlayerEntity
-import net.minecraft.fluid.Fluid
-import net.minecraft.item.FuelRegistry
-import net.minecraft.item.map.MapState
-import net.minecraft.particle.ParticleEffect
-import net.minecraft.recipe.BrewingRecipeRegistry
-import net.minecraft.recipe.RecipeManager
-import net.minecraft.recipe.RecipePropertySet
-import net.minecraft.recipe.StonecuttingRecipe
-import net.minecraft.recipe.display.CuttingRecipeDisplay
-import net.minecraft.registry.DynamicRegistryManager
-import net.minecraft.registry.Registries
-import net.minecraft.registry.RegistryKey
-import net.minecraft.registry.RegistryKeys
-import net.minecraft.registry.ServerDynamicRegistryType
-import net.minecraft.registry.entry.RegistryEntry
-import net.minecraft.resource.DataConfiguration
-import net.minecraft.resource.ResourcePackManager
-import net.minecraft.resource.featuretoggle.FeatureFlags
-import net.minecraft.resource.featuretoggle.FeatureSet
-import net.minecraft.scoreboard.Scoreboard
-import net.minecraft.server.SaveLoading
-import net.minecraft.server.command.CommandManager
-import net.minecraft.sound.SoundCategory
-import net.minecraft.sound.SoundEvent
-import net.minecraft.util.Identifier
-import net.minecraft.util.TypeFilter
-import net.minecraft.util.function.LazyIterationConsumer
-import net.minecraft.util.math.BlockPos
-import net.minecraft.util.math.Box
-import net.minecraft.util.math.ChunkPos
-import net.minecraft.util.math.Direction
-import net.minecraft.util.math.Vec3d
-import net.minecraft.world.BlockView
-import net.minecraft.world.Difficulty
-import net.minecraft.world.MutableWorldProperties
-import net.minecraft.world.World
-import net.minecraft.world.biome.Biome
-import net.minecraft.world.biome.BiomeKeys
-import net.minecraft.world.chunk.Chunk
-import net.minecraft.world.chunk.ChunkManager
-import net.minecraft.world.chunk.ChunkStatus
-import net.minecraft.world.chunk.EmptyChunk
-import net.minecraft.world.chunk.light.LightingProvider
-import net.minecraft.world.entity.EntityLookup
-import net.minecraft.world.event.GameEvent
-import net.minecraft.world.explosion.ExplosionBehavior
-import net.minecraft.world.tick.OrderedTick
-import net.minecraft.world.tick.QueryableTickScheduler
-import net.minecraft.world.tick.TickManager
-import moe.nea.firmament.util.MC
-
-fun createDynamicRegistry(): DynamicRegistryManager.Immutable {
- // TODO: use SaveLoading.load() to properly load a full registry
- return DynamicRegistryManager.of(Registries.REGISTRIES)
-}
-
-class FakeWorld(
- registries: DynamicRegistryManager.Immutable = createDynamicRegistry(),
-) : World(
- Properties,
- RegistryKey.of(RegistryKeys.WORLD, Identifier.of("firmament", "fakeworld")),
- registries,
- MC.defaultRegistries.getOrThrow(RegistryKeys.DIMENSION_TYPE)
- .getOrThrow(RegistryKey.of(RegistryKeys.DIMENSION_TYPE, Identifier.of("minecraft", "overworld"))),
- true,
- false,
- 0L,
- 0
-) {
- object Properties : MutableWorldProperties {
- override fun getSpawnPos(): BlockPos {
- return BlockPos.ORIGIN
- }
-
- override fun getSpawnAngle(): Float {
- return 0F
- }
-
- override fun getTime(): Long {
- return 0
- }
-
- override fun getTimeOfDay(): Long {
- return 0
- }
-
- override fun isThundering(): Boolean {
- return false
- }
-
- override fun isRaining(): Boolean {
- return false
- }
-
- override fun setRaining(raining: Boolean) {
- }
-
- override fun isHardcore(): Boolean {
- return false
- }
-
- override fun getDifficulty(): Difficulty {
- return Difficulty.HARD
- }
-
- override fun isDifficultyLocked(): Boolean {
- return false
- }
-
- override fun setSpawnPos(pos: BlockPos?, angle: Float) {}
- }
-
- override fun getPlayers(): List<PlayerEntity> {
- return emptyList()
- }
-
- override fun getBrightness(direction: Direction?, shaded: Boolean): Float {
- return 1f
- }
-
- override fun getGeneratorStoredBiome(biomeX: Int, biomeY: Int, biomeZ: Int): RegistryEntry<Biome> {
- return registryManager.getOptionalEntry(BiomeKeys.PLAINS).get()
- }
-
- override fun getSeaLevel(): Int {
- return 0
- }
-
- override fun getEnabledFeatures(): FeatureSet {
- return FeatureFlags.VANILLA_FEATURES
- }
-
- class FakeTickScheduler<T> : QueryableTickScheduler<T> {
- override fun scheduleTick(orderedTick: OrderedTick<T>?) {
- }
-
- override fun isQueued(pos: BlockPos?, type: T): Boolean {
- return true
- }
-
- override fun getTickCount(): Int {
- return 0
- }
-
- override fun isTicking(pos: BlockPos?, type: T): Boolean {
- return true
- }
-
- }
-
- override fun getBlockTickScheduler(): QueryableTickScheduler<Block> {
- return FakeTickScheduler()
- }
-
- override fun getFluidTickScheduler(): QueryableTickScheduler<Fluid> {
- return FakeTickScheduler()
- }
-
-
- class FakeChunkManager(val world: FakeWorld) : ChunkManager() {
- override fun getChunk(x: Int, z: Int, leastStatus: ChunkStatus?, create: Boolean): Chunk {
- return EmptyChunk(
- world,
- ChunkPos(x, z),
- world.registryManager.getOptionalEntry(BiomeKeys.PLAINS).get()
- )
- }
-
- override fun getWorld(): BlockView {
- return world
- }
-
- override fun tick(shouldKeepTicking: BooleanSupplier?, tickChunks: Boolean) {
- }
-
- override fun getDebugString(): String {
- return "FakeChunkManager"
- }
-
- override fun getLoadedChunkCount(): Int {
- return 0
- }
-
- override fun getLightingProvider(): LightingProvider {
- return FakeLightingProvider(this)
- }
- }
-
- class FakeLightingProvider(chunkManager: FakeChunkManager) : LightingProvider(chunkManager, false, false)
-
- override fun getChunkManager(): ChunkManager {
- return FakeChunkManager(this)
- }
-
- override fun playSound(
- source: PlayerEntity?,
- x: Double,
- y: Double,
- z: Double,
- sound: RegistryEntry<SoundEvent>?,
- category: SoundCategory?,
- volume: Float,
- pitch: Float,
- seed: Long
- ) {
- }
-
- override fun syncWorldEvent(player: PlayerEntity?, eventId: Int, pos: BlockPos?, data: Int) {
- }
-
- override fun emitGameEvent(event: RegistryEntry<GameEvent>?, emitterPos: Vec3d?, emitter: GameEvent.Emitter?) {
- }
-
- override fun updateListeners(pos: BlockPos?, oldState: BlockState?, newState: BlockState?, flags: Int) {
- }
-
- override fun playSoundFromEntity(
- source: PlayerEntity?,
- entity: Entity?,
- sound: RegistryEntry<SoundEvent>?,
- category: SoundCategory?,
- volume: Float,
- pitch: Float,
- seed: Long
- ) {
- }
-
- override fun createExplosion(
- entity: Entity?,
- damageSource: DamageSource?,
- behavior: ExplosionBehavior?,
- x: Double,
- y: Double,
- z: Double,
- power: Float,
- createFire: Boolean,
- explosionSourceType: ExplosionSourceType?,
- smallParticle: ParticleEffect?,
- largeParticle: ParticleEffect?,
- soundEvent: RegistryEntry<SoundEvent>?
- ) {
- TODO("Not yet implemented")
- }
-
- override fun asString(): String {
- return "FakeWorld"
- }
-
- override fun getEntityById(id: Int): Entity? {
- return null
- }
-
- override fun getEnderDragonParts(): MutableCollection<EnderDragonPart> {
- return mutableListOf()
- }
-
- override fun getTickManager(): TickManager {
- return TickManager()
- }
-
- override fun getMapState(id: MapIdComponent?): MapState? {
- return null
- }
-
- override fun putMapState(id: MapIdComponent?, state: MapState?) {
- }
-
- override fun increaseAndGetMapId(): MapIdComponent {
- return MapIdComponent(0)
- }
-
- override fun setBlockBreakingInfo(entityId: Int, pos: BlockPos?, progress: Int) {
- }
-
- override fun getScoreboard(): Scoreboard {
- return Scoreboard()
- }
-
- override fun getRecipeManager(): RecipeManager {
- return object : RecipeManager {
- override fun getPropertySet(key: RegistryKey<RecipePropertySet>?): RecipePropertySet {
- return RecipePropertySet.EMPTY
- }
-
- override fun getStonecutterRecipes(): CuttingRecipeDisplay.Grouping<StonecuttingRecipe> {
- return CuttingRecipeDisplay.Grouping.empty()
- }
- }
- }
-
- object FakeEntityLookup : EntityLookup<Entity> {
- override fun get(id: Int): Entity? {
- return null
- }
-
- override fun get(uuid: UUID?): Entity? {
- return null
- }
-
- override fun iterate(): MutableIterable<Entity> {
- return mutableListOf()
- }
-
- override fun <U : Entity?> forEachIntersects(
- filter: TypeFilter<Entity, U>?,
- box: Box?,
- consumer: LazyIterationConsumer<U>?
- ) {
- }
-
- override fun forEachIntersects(box: Box?, action: Consumer<Entity>?) {
- }
-
- override fun <U : Entity?> forEach(filter: TypeFilter<Entity, U>?, consumer: LazyIterationConsumer<U>?) {
- }
-
- }
-
- override fun getEntityLookup(): EntityLookup<Entity> {
- return FakeEntityLookup
- }
-
- override fun getBrewingRecipeRegistry(): BrewingRecipeRegistry {
- return BrewingRecipeRegistry.EMPTY
- }
-
- override fun getFuelRegistry(): FuelRegistry {
- TODO("Not yet implemented")
- }
-}
diff --git a/src/main/kotlin/gui/entity/ModifyEquipment.kt b/src/main/kotlin/gui/entity/ModifyEquipment.kt
index a558936..2ef5007 100644
--- a/src/main/kotlin/gui/entity/ModifyEquipment.kt
+++ b/src/main/kotlin/gui/entity/ModifyEquipment.kt
@@ -8,10 +8,11 @@ import net.minecraft.entity.LivingEntity
import net.minecraft.item.Item
import net.minecraft.item.ItemStack
import net.minecraft.item.Items
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.SBItemStack
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.mc.setEncodedSkullOwner
-import moe.nea.firmament.util.mc.zeroUUID
+import moe.nea.firmament.util.mc.arbitraryUUID
object ModifyEquipment : EntityModifier {
val names = mapOf(
@@ -31,12 +32,13 @@ object ModifyEquipment : EntityModifier {
return entity
}
+ @OptIn(ExpensiveItemCacheApi::class)
private fun createItem(item: String): ItemStack {
val split = item.split("#")
if (split.size != 2) return SBItemStack(SkyblockId(item)).asImmutableItemStack()
val (type, data) = split
return when (type) {
- "SKULL" -> ItemStack(Items.PLAYER_HEAD).also { it.setEncodedSkullOwner(zeroUUID, data) }
+ "SKULL" -> ItemStack(Items.PLAYER_HEAD).also { it.setEncodedSkullOwner(arbitraryUUID, data) }
"LEATHER_LEGGINGS" -> coloredLeatherArmor(Items.LEATHER_LEGGINGS, data)
"LEATHER_BOOTS" -> coloredLeatherArmor(Items.LEATHER_BOOTS, data)
"LEATHER_HELMET" -> coloredLeatherArmor(Items.LEATHER_HELMET, data)
@@ -47,7 +49,7 @@ object ModifyEquipment : EntityModifier {
private fun coloredLeatherArmor(leatherArmor: Item, data: String): ItemStack {
val stack = ItemStack(leatherArmor)
- stack.set(DataComponentTypes.DYED_COLOR, DyedColorComponent(data.toInt(16), false))
+ stack.set(DataComponentTypes.DYED_COLOR, DyedColorComponent(data.toInt(16)))
return stack
}
}
diff --git a/src/main/kotlin/gui/entity/ModifyHorse.kt b/src/main/kotlin/gui/entity/ModifyHorse.kt
index f094ca4..7c8baa7 100644
--- a/src/main/kotlin/gui/entity/ModifyHorse.kt
+++ b/src/main/kotlin/gui/entity/ModifyHorse.kt
@@ -1,4 +1,3 @@
-
package moe.nea.firmament.gui.entity
import com.google.gson.JsonNull
@@ -7,6 +6,7 @@ import kotlin.experimental.and
import kotlin.experimental.inv
import kotlin.experimental.or
import net.minecraft.entity.EntityType
+import net.minecraft.entity.EquipmentSlot
import net.minecraft.entity.LivingEntity
import net.minecraft.entity.SpawnReason
import net.minecraft.entity.passive.AbstractHorseEntity
@@ -15,48 +15,45 @@ import net.minecraft.item.Items
import moe.nea.firmament.gui.entity.EntityRenderer.fakeWorld
object ModifyHorse : EntityModifier {
- override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
- require(entity is AbstractHorseEntity)
- var entity: AbstractHorseEntity = entity
- info["kind"]?.let {
- entity = when (it.asString) {
- "skeleton" -> EntityType.SKELETON_HORSE.create(fakeWorld, SpawnReason.LOAD)!!
- "zombie" -> EntityType.ZOMBIE_HORSE.create(fakeWorld, SpawnReason.LOAD)!!
- "mule" -> EntityType.MULE.create(fakeWorld, SpawnReason.LOAD)!!
- "donkey" -> EntityType.DONKEY.create(fakeWorld, SpawnReason.LOAD)!!
- "horse" -> EntityType.HORSE.create(fakeWorld, SpawnReason.LOAD)!!
- else -> error("Unknown horse kind $it")
- }
- }
- info["armor"]?.let {
- if (it is JsonNull) {
- entity.setHorseArmor(ItemStack.EMPTY)
- } else {
- when (it.asString) {
- "iron" -> entity.setHorseArmor(ItemStack(Items.IRON_HORSE_ARMOR))
- "golden" -> entity.setHorseArmor(ItemStack(Items.GOLDEN_HORSE_ARMOR))
- "diamond" -> entity.setHorseArmor(ItemStack(Items.DIAMOND_HORSE_ARMOR))
- else -> error("Unknown horse armor $it")
- }
- }
- }
- info["saddled"]?.let {
- entity.setIsSaddled(it.asBoolean)
- }
- return entity
- }
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ require(entity is AbstractHorseEntity)
+ var entity: AbstractHorseEntity = entity
+ info["kind"]?.let {
+ entity = when (it.asString) {
+ "skeleton" -> EntityType.SKELETON_HORSE.create(fakeWorld, SpawnReason.LOAD)!!
+ "zombie" -> EntityType.ZOMBIE_HORSE.create(fakeWorld, SpawnReason.LOAD)!!
+ "mule" -> EntityType.MULE.create(fakeWorld, SpawnReason.LOAD)!!
+ "donkey" -> EntityType.DONKEY.create(fakeWorld, SpawnReason.LOAD)!!
+ "horse" -> EntityType.HORSE.create(fakeWorld, SpawnReason.LOAD)!!
+ else -> error("Unknown horse kind $it")
+ }
+ }
+ info["armor"]?.let {
+ if (it is JsonNull) {
+ entity.setHorseArmor(ItemStack.EMPTY)
+ } else {
+ when (it.asString) {
+ "iron" -> entity.setHorseArmor(ItemStack(Items.IRON_HORSE_ARMOR))
+ "golden" -> entity.setHorseArmor(ItemStack(Items.GOLDEN_HORSE_ARMOR))
+ "diamond" -> entity.setHorseArmor(ItemStack(Items.DIAMOND_HORSE_ARMOR))
+ else -> error("Unknown horse armor $it")
+ }
+ }
+ }
+ info["saddled"]?.let {
+ entity.setIsSaddled(it.asBoolean)
+ }
+ return entity
+ }
}
fun AbstractHorseEntity.setIsSaddled(shouldBeSaddled: Boolean) {
- val oldFlag = dataTracker.get(AbstractHorseEntity.HORSE_FLAGS)
- dataTracker.set(
- AbstractHorseEntity.HORSE_FLAGS,
- if (shouldBeSaddled) oldFlag or AbstractHorseEntity.SADDLED_FLAG.toByte()
- else oldFlag and AbstractHorseEntity.SADDLED_FLAG.toByte().inv()
- )
+ this.equipStack(EquipmentSlot.SADDLE,
+ if (shouldBeSaddled) ItemStack(Items.SADDLE)
+ else ItemStack.EMPTY)
}
fun AbstractHorseEntity.setHorseArmor(itemStack: ItemStack) {
- items.setStack(1, itemStack)
+ this.equipBodyArmor(itemStack)
}
diff --git a/src/main/kotlin/keybindings/IKeyBinding.kt b/src/main/kotlin/keybindings/IKeyBinding.kt
index 1975361..9d9b106 100644
--- a/src/main/kotlin/keybindings/IKeyBinding.kt
+++ b/src/main/kotlin/keybindings/IKeyBinding.kt
@@ -6,24 +6,45 @@ import net.minecraft.client.option.KeyBinding
interface IKeyBinding {
fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean
+ fun matchesAtLeast(keyCode: Int, scanCode: Int, modifiers: Int): Boolean
fun withModifiers(wantedModifiers: Int): IKeyBinding {
val old = this
return object : IKeyBinding {
override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
- return old.matches(keyCode, scanCode, modifiers) && (modifiers and wantedModifiers) == wantedModifiers
+ return old.matchesAtLeast(keyCode, scanCode, modifiers) && (modifiers and wantedModifiers) == wantedModifiers
}
- }
+
+ override fun matchesAtLeast(
+ keyCode: Int,
+ scanCode: Int,
+ modifiers: Int
+ ): Boolean {
+ return old.matchesAtLeast(keyCode, scanCode, modifiers) && (modifiers.inv() and wantedModifiers) == 0
+ }
+ }
}
companion object {
fun minecraft(keyBinding: KeyBinding) = object : IKeyBinding {
override fun matches(keyCode: Int, scanCode: Int, modifiers: Int) =
keyBinding.matchesKey(keyCode, scanCode)
- }
+
+ override fun matchesAtLeast(
+ keyCode: Int,
+ scanCode: Int,
+ modifiers: Int
+ ): Boolean =
+ keyBinding.matchesKey(keyCode, scanCode)
+ }
fun ofKeyCode(wantedKeyCode: Int) = object : IKeyBinding {
- override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean = keyCode == wantedKeyCode
- }
+ override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean = keyCode == wantedKeyCode && modifiers == 0
+ override fun matchesAtLeast(
+ keyCode: Int,
+ scanCode: Int,
+ modifiers: Int
+ ): Boolean = keyCode == wantedKeyCode
+ }
}
}
diff --git a/src/main/kotlin/keybindings/SavedKeyBinding.kt b/src/main/kotlin/keybindings/SavedKeyBinding.kt
index 5bca87e..01baa8f 100644
--- a/src/main/kotlin/keybindings/SavedKeyBinding.kt
+++ b/src/main/kotlin/keybindings/SavedKeyBinding.kt
@@ -1,5 +1,3 @@
-
-
package moe.nea.firmament.keybindings
import org.lwjgl.glfw.GLFW
@@ -8,99 +6,120 @@ import net.minecraft.client.MinecraftClient
import net.minecraft.client.util.InputUtil
import net.minecraft.text.Text
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.mc.InitLevel
+// TODO: add support for mouse keybindings
@Serializable
data class SavedKeyBinding(
- val keyCode: Int,
- val shift: Boolean = false,
- val ctrl: Boolean = false,
- val alt: Boolean = false,
+ val keyCode: Int,
+ val shift: Boolean = false,
+ val ctrl: Boolean = false,
+ val alt: Boolean = false,
) : IKeyBinding {
- val isBound: Boolean get() = keyCode != GLFW.GLFW_KEY_UNKNOWN
-
- constructor(keyCode: Int, mods: Triple<Boolean, Boolean, Boolean>) : this(
- keyCode,
- mods.first && keyCode != GLFW.GLFW_KEY_LEFT_SHIFT && keyCode != GLFW.GLFW_KEY_RIGHT_SHIFT,
- mods.second && keyCode != GLFW.GLFW_KEY_LEFT_CONTROL && keyCode != GLFW.GLFW_KEY_RIGHT_CONTROL,
- mods.third && keyCode != GLFW.GLFW_KEY_LEFT_ALT && keyCode != GLFW.GLFW_KEY_RIGHT_ALT,
- )
-
- constructor(keyCode: Int, mods: Int) : this(keyCode, getMods(mods))
-
- companion object {
- fun getMods(modifiers: Int): Triple<Boolean, Boolean, Boolean> {
- return Triple(
- modifiers and GLFW.GLFW_MOD_SHIFT != 0,
- modifiers and GLFW.GLFW_MOD_CONTROL != 0,
- modifiers and GLFW.GLFW_MOD_ALT != 0,
- )
- }
-
- fun getModInt(): Int {
- val h = MC.window.handle
- val ctrl = if (MinecraftClient.IS_SYSTEM_MAC) {
- InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_SUPER)
- || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_SUPER)
- } else InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_CONTROL)
- || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_CONTROL)
- val shift = isShiftDown()
- val alt = InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_ALT)
- || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_ALT)
- var mods = 0
- if (ctrl) mods = mods or GLFW.GLFW_MOD_CONTROL
- if (shift) mods = mods or GLFW.GLFW_MOD_SHIFT
- if (alt) mods = mods or GLFW.GLFW_MOD_ALT
- return mods
- }
-
- private val h get() = MC.window.handle
- fun isShiftDown() = InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_SHIFT)
- || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_SHIFT)
-
- }
-
- fun isPressed(atLeast: Boolean = false): Boolean {
- if (!isBound) return false
- val h = MC.window.handle
- if (!InputUtil.isKeyPressed(h, keyCode)) return false
-
- val ctrl = if (MinecraftClient.IS_SYSTEM_MAC) {
- InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_SUPER)
- || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_SUPER)
- } else InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_CONTROL)
- || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_CONTROL)
- val shift = isShiftDown()
- val alt = InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_ALT)
- || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_ALT)
- if (atLeast)
- return (ctrl >= this.ctrl) &&
- (alt >= this.alt) &&
- (shift >= this.shift)
-
- return (ctrl == this.ctrl) &&
- (alt == this.alt) &&
- (shift == this.shift)
- }
-
- override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
- if (this.keyCode == GLFW.GLFW_KEY_UNKNOWN) return false
- return keyCode == this.keyCode && getMods(modifiers) == Triple(shift, ctrl, alt)
- }
-
- fun format(): Text {
- val stroke = Text.literal("")
- if (ctrl) {
- stroke.append("CTRL + ")
- }
- if (alt) {
- stroke.append("ALT + ")
- }
- if (shift) {
- stroke.append("SHIFT + ") // TODO: translations?
- }
-
- stroke.append(InputUtil.Type.KEYSYM.createFromCode(keyCode).localizedText)
- return stroke
- }
+ val isBound: Boolean get() = keyCode != GLFW.GLFW_KEY_UNKNOWN
+
+ constructor(keyCode: Int, mods: Triple<Boolean, Boolean, Boolean>) : this(
+ keyCode,
+ mods.first && keyCode != GLFW.GLFW_KEY_LEFT_SHIFT && keyCode != GLFW.GLFW_KEY_RIGHT_SHIFT,
+ mods.second && keyCode != GLFW.GLFW_KEY_LEFT_CONTROL && keyCode != GLFW.GLFW_KEY_RIGHT_CONTROL,
+ mods.third && keyCode != GLFW.GLFW_KEY_LEFT_ALT && keyCode != GLFW.GLFW_KEY_RIGHT_ALT,
+ )
+
+ constructor(keyCode: Int, mods: Int) : this(keyCode, getMods(mods))
+
+ companion object {
+ fun getMods(modifiers: Int): Triple<Boolean, Boolean, Boolean> {
+ return Triple(
+ modifiers and GLFW.GLFW_MOD_SHIFT != 0,
+ modifiers and GLFW.GLFW_MOD_CONTROL != 0,
+ modifiers and GLFW.GLFW_MOD_ALT != 0,
+ )
+ }
+
+ fun getModInt(): Int {
+ val h = MC.window.handle
+ val ctrl = if (MinecraftClient.IS_SYSTEM_MAC) {
+ InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_SUPER)
+ || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_SUPER)
+ } else InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_CONTROL)
+ || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_CONTROL)
+ val shift = isShiftDown()
+ val alt = InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_ALT)
+ || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_ALT)
+ var mods = 0
+ if (ctrl) mods = mods or GLFW.GLFW_MOD_CONTROL
+ if (shift) mods = mods or GLFW.GLFW_MOD_SHIFT
+ if (alt) mods = mods or GLFW.GLFW_MOD_ALT
+ return mods
+ }
+
+ private val h get() = MC.window.handle
+ fun isShiftDown() = shiftKeys.any { InputUtil.isKeyPressed(h, it) }
+
+ fun unbound(): SavedKeyBinding =
+ SavedKeyBinding(GLFW.GLFW_KEY_UNKNOWN)
+
+ val controlKeys = if (MinecraftClient.IS_SYSTEM_MAC) {
+ listOf(GLFW.GLFW_KEY_LEFT_SUPER, GLFW.GLFW_KEY_RIGHT_SUPER)
+ } else {
+ listOf(GLFW.GLFW_KEY_LEFT_CONTROL, GLFW.GLFW_KEY_RIGHT_CONTROL)
+ }
+ val shiftKeys = listOf(GLFW.GLFW_KEY_LEFT_SHIFT, GLFW.GLFW_KEY_RIGHT_SHIFT)
+
+ val altKeys = listOf(GLFW.GLFW_KEY_LEFT_ALT, GLFW.GLFW_KEY_RIGHT_ALT)
+ }
+
+ fun isPressed(atLeast: Boolean = false): Boolean {
+ if (!isBound) return false
+ val h = MC.window.handle
+ if (!InputUtil.isKeyPressed(h, keyCode)) return false
+
+ // These are modifiers, so if the searched keyCode is a modifier key, then that key does not count as the modifier
+ val ctrl = keyCode !in controlKeys && controlKeys.any { InputUtil.isKeyPressed(h, it) }
+ val shift = keyCode !in shiftKeys && isShiftDown()
+ val alt = keyCode !in altKeys && altKeys.any { InputUtil.isKeyPressed(h, it) }
+ if (atLeast)
+ return (ctrl >= this.ctrl) &&
+ (alt >= this.alt) &&
+ (shift >= this.shift)
+
+ return (ctrl == this.ctrl) &&
+ (alt == this.alt) &&
+ (shift == this.shift)
+ }
+
+ override fun matchesAtLeast(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
+ if (this.keyCode == GLFW.GLFW_KEY_UNKNOWN) return false
+ val (shift, ctrl, alt) = getMods(modifiers)
+ return keyCode == this.keyCode && this.shift <= shift && this.ctrl <= ctrl && this.alt <= alt
+ }
+
+ override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
+ if (this.keyCode == GLFW.GLFW_KEY_UNKNOWN) return false
+ return keyCode == this.keyCode && getMods(modifiers) == Triple(shift, ctrl, alt)
+ }
+
+ override fun toString(): String {
+ return format().string
+ }
+
+ fun format(): Text {
+ val stroke = Text.literal("")
+ if (ctrl) {
+ stroke.append("CTRL + ")
+ }
+ if (alt) {
+ stroke.append("ALT + ")
+ }
+ if (shift) {
+ stroke.append("SHIFT + ") // TODO: translations?
+ }
+ if (InitLevel.isAtLeast(InitLevel.RENDER_INIT)) {
+ stroke.append(InputUtil.Type.KEYSYM.createFromCode(keyCode).localizedText)
+ } else {
+ stroke.append(keyCode.toString())
+ }
+ return stroke
+ }
}
diff --git a/src/main/kotlin/repo/ExpLadder.kt b/src/main/kotlin/repo/ExpLadder.kt
index fbc9eb8..25a74de 100644
--- a/src/main/kotlin/repo/ExpLadder.kt
+++ b/src/main/kotlin/repo/ExpLadder.kt
@@ -19,7 +19,8 @@ object ExpLadders : IReloadable {
val expInCurrentLevel: Float,
var expTotal: Float,
) {
- val percentageToNextLevel: Float = expInCurrentLevel / expRequiredForNextLevel
+ val percentageToNextLevel: Float = expInCurrentLevel / expRequiredForNextLevel
+ val percentageToMaxLevel: Float = expTotal / expRequiredForMaxLevel
}
data class ExpLadder(
diff --git a/src/main/kotlin/repo/ExpensiveItemCacheApi.kt b/src/main/kotlin/repo/ExpensiveItemCacheApi.kt
new file mode 100644
index 0000000..eef95a6
--- /dev/null
+++ b/src/main/kotlin/repo/ExpensiveItemCacheApi.kt
@@ -0,0 +1,8 @@
+package moe.nea.firmament.repo
+
+/**
+ * Marker for functions that could potentially invoke DFU. Please do not call on a lot of objects at once, or try to make sure the item is cached and fall back to a more gentle function call using [SBItemStack.isWarm] and similar functions.
+ */
+@RequiresOptIn
+@Retention(AnnotationRetention.BINARY)
+annotation class ExpensiveItemCacheApi
diff --git a/src/main/kotlin/repo/HypixelStaticData.kt b/src/main/kotlin/repo/HypixelStaticData.kt
index 181aa70..b0ada77 100644
--- a/src/main/kotlin/repo/HypixelStaticData.kt
+++ b/src/main/kotlin/repo/HypixelStaticData.kt
@@ -3,21 +3,17 @@ package moe.nea.firmament.repo
import io.ktor.client.call.body
import io.ktor.client.request.get
import org.apache.logging.log4j.LogManager
-import org.lwjgl.glfw.GLFW
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.time.Duration.Companion.minutes
import moe.nea.firmament.Firmament
import moe.nea.firmament.apis.CollectionResponse
import moe.nea.firmament.apis.CollectionSkillData
-import moe.nea.firmament.keybindings.IKeyBinding
import moe.nea.firmament.util.SkyblockId
-import moe.nea.firmament.util.async.waitForInput
object HypixelStaticData {
private val logger = LogManager.getLogger("Firmament.HypixelStaticData")
@@ -25,7 +21,13 @@ object HypixelStaticData {
private val hypixelApiBaseUrl = "https://api.hypixel.net"
var lowestBin: Map<SkyblockId, Double> = mapOf()
private set
- var bazaarData: Map<SkyblockId, BazaarData> = mapOf()
+ var avg1dlowestBin: Map<SkyblockId, Double> = mapOf()
+ private set
+ var avg3dlowestBin: Map<SkyblockId, Double> = mapOf()
+ private set
+ var avg7dlowestBin: Map<SkyblockId, Double> = mapOf()
+ private set
+ var bazaarData: Map<SkyblockId.BazaarStock, BazaarData> = mapOf()
private set
var collectionData: Map<String, CollectionSkillData> = mapOf()
private set
@@ -56,9 +58,10 @@ object HypixelStaticData {
val products: Map<SkyblockId.BazaarStock, BazaarData> = mapOf(),
)
- fun getPriceOfItem(item: SkyblockId): Double? = bazaarData[item]?.quickStatus?.buyPrice ?: lowestBin[item]
- fun hasBazaarStock(item: SkyblockId): Boolean {
+ fun getPriceOfItem(item: SkyblockId): Double? = bazaarData[SkyblockId.BazaarStock.fromSkyBlockId(item)]?.quickStatus?.buyPrice ?: lowestBin[item]
+
+ fun hasBazaarStock(item: SkyblockId.BazaarStock): Boolean {
return item in bazaarData
}
@@ -90,6 +93,12 @@ object HypixelStaticData {
private suspend fun fetchPricesFromMoulberry() {
lowestBin = Firmament.httpClient.get("$moulberryBaseUrl/lowestbin.json")
.body<Map<SkyblockId, Double>>()
+ avg1dlowestBin = Firmament.httpClient.get("$moulberryBaseUrl/auction_averages_lbin/1day.json")
+ .body<Map<SkyblockId, Double>>()
+ avg3dlowestBin = Firmament.httpClient.get("$moulberryBaseUrl/auction_averages_lbin/3day.json")
+ .body<Map<SkyblockId, Double>>()
+ avg7dlowestBin = Firmament.httpClient.get("$moulberryBaseUrl/auction_averages_lbin/7day.json")
+ .body<Map<SkyblockId, Double>>()
}
private suspend fun fetchBazaarPrices() {
@@ -97,7 +106,7 @@ object HypixelStaticData {
if (!response.success) {
logger.warn("Retrieved unsuccessful bazaar data")
}
- bazaarData = response.products.mapKeys { it.key.toRepoId() }
+ bazaarData = response.products
}
private suspend fun updateCollectionData() {
diff --git a/src/main/kotlin/repo/ItemCache.kt b/src/main/kotlin/repo/ItemCache.kt
index 0967ad1..14decd8 100644
--- a/src/main/kotlin/repo/ItemCache.kt
+++ b/src/main/kotlin/repo/ItemCache.kt
@@ -4,16 +4,21 @@ import com.mojang.serialization.Dynamic
import io.github.moulberry.repo.IReloadable
import io.github.moulberry.repo.NEURepository
import io.github.moulberry.repo.data.NEUItem
-import io.github.notenoughupdates.moulconfig.xml.Bind
import java.text.NumberFormat
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import org.apache.logging.log4j.LogManager
-import kotlinx.coroutines.Job
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlin.io.path.readText
import kotlin.jvm.optionals.getOrNull
import net.minecraft.SharedConstants
-import net.minecraft.client.resource.language.I18n
import net.minecraft.component.DataComponentTypes
import net.minecraft.component.type.NbtComponent
import net.minecraft.datafixer.Schemas
@@ -24,17 +29,18 @@ import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtElement
import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.NbtString
+import net.minecraft.nbt.StringNbtReader
import net.minecraft.text.MutableText
import net.minecraft.text.Style
import net.minecraft.text.Text
+import net.minecraft.util.Identifier
import moe.nea.firmament.Firmament
-import moe.nea.firmament.gui.config.HudMeta
-import moe.nea.firmament.gui.config.HudPosition
-import moe.nea.firmament.gui.hud.MoulConfigHud
+import moe.nea.firmament.features.debug.ExportedTestConstantMeta
import moe.nea.firmament.repo.RepoManager.initialize
import moe.nea.firmament.util.LegacyFormattingCode
import moe.nea.firmament.util.LegacyTagParser
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.MinecraftDispatcher
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.TestUtil
import moe.nea.firmament.util.directLiteralStringContent
@@ -45,6 +51,7 @@ import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.mc.modifyLore
import moe.nea.firmament.util.mc.setCustomName
import moe.nea.firmament.util.mc.setSkullOwner
+import moe.nea.firmament.util.skyblockId
import moe.nea.firmament.util.transformEachRecursively
object ItemCache : IReloadable {
@@ -61,14 +68,18 @@ object ItemCache : IReloadable {
putShort("Damage", damage.toShort())
}
+ @ExpensiveItemCacheApi
private fun NbtCompound.transformFrom10809ToModern() = convert189ToModern(this@transformFrom10809ToModern)
+ val currentSaveVersion = SharedConstants.getGameVersion().saveVersion.id
+
+ @ExpensiveItemCacheApi
fun convert189ToModern(nbtComponent: NbtCompound): NbtCompound? =
try {
df.update(
TypeReferences.ITEM_STACK,
Dynamic(NbtOps.INSTANCE, nbtComponent),
-1,
- SharedConstants.getGameVersion().saveVersion.id
+ currentSaveVersion
).value as NbtCompound
} catch (e: Exception) {
isFlawless = false
@@ -131,18 +142,48 @@ object ItemCache : IReloadable {
return base
}
+ fun tryFindFromModernFormat(skyblockId: SkyblockId): NbtCompound? {
+ val overlayFile =
+ RepoManager.overlayData.getMostModernReadableOverlay(skyblockId, currentSaveVersion) ?: return null
+ val overlay = StringNbtReader.readCompound(overlayFile.path.readText())
+ val result = ExportedTestConstantMeta.SOURCE_CODEC.decode(
+ NbtOps.INSTANCE, overlay
+ ).result().getOrNull() ?: return null
+ val meta = result.first
+ return df.update(
+ TypeReferences.ITEM_STACK,
+ Dynamic(NbtOps.INSTANCE, result.second),
+ meta.dataVersion,
+ currentSaveVersion
+ ).value as NbtCompound
+ }
+
+ @ExpensiveItemCacheApi
private fun NEUItem.asItemStackNow(): ItemStack {
+
try {
+ var modernItemTag = tryFindFromModernFormat(this.skyblockId)
val oldItemTag = get10809CompoundTag()
- val modernItemTag = oldItemTag.transformFrom10809ToModern()
- ?: return brokenItemStack(this)
+ var usedOldNbt = false
+ if (modernItemTag == null) {
+ usedOldNbt = true
+ modernItemTag = oldItemTag.transformFrom10809ToModern()
+ ?: return brokenItemStack(this)
+ }
val itemInstance =
ItemStack.fromNbt(MC.defaultRegistries, modernItemTag).getOrNull() ?: return brokenItemStack(this)
+ if (usedOldNbt) {
+ val tag = oldItemTag.getCompound("tag")
+ val extraAttributes = tag.flatMap { it.getCompound("ExtraAttributes") }
+ .getOrNull()
+ if (extraAttributes != null)
+ itemInstance.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(extraAttributes))
+ val itemModel = tag.flatMap { it.getString("ItemModel") }.getOrNull()
+ if (itemModel != null)
+ itemInstance.set(DataComponentTypes.ITEM_MODEL, Identifier.of(itemModel))
+ }
itemInstance.loreAccordingToNbt = lore.map { un189Lore(it) }
itemInstance.displayNameAccordingToNbt = un189Lore(displayName)
- val extraAttributes = oldItemTag.getCompound("tag").getCompound("ExtraAttributes")
- if (extraAttributes != null)
- itemInstance.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(extraAttributes))
return itemInstance
} catch (e: Exception) {
e.printStackTrace()
@@ -150,6 +191,11 @@ object ItemCache : IReloadable {
}
}
+ fun hasCacheFor(skyblockId: SkyblockId): Boolean {
+ return skyblockId.neuItem in cache
+ }
+
+ @ExpensiveItemCacheApi
fun NEUItem?.asItemStack(idHint: SkyblockId? = null, loreReplacements: Map<String, String>? = null): ItemStack {
if (this == null) return brokenItemStack(null, idHint)
var s = cache[this.skyblockItemId]
@@ -183,22 +229,49 @@ object ItemCache : IReloadable {
}
}
- var job: Job? = null
+ var itemRecacheScope: CoroutineScope? = null
- override fun reload(repository: NEURepository) {
- val j = job
- if (j != null && j.isActive) {
- j.cancel()
+ private var recacheSoonSubmitted = mutableSetOf<SkyblockId>()
+
+ @OptIn(ExpensiveItemCacheApi::class)
+ fun recacheSoon(neuItem: NEUItem) {
+ itemRecacheScope?.launch {
+ if (!withContext(MinecraftDispatcher) {
+ recacheSoonSubmitted.add(neuItem.skyblockId)
+ }) {
+ return@launch
+ }
+ neuItem.asItemStack()
}
+ }
+
+ @OptIn(ExpensiveItemCacheApi::class)
+ override fun reload(repository: NEURepository) {
+ val j = itemRecacheScope
+ j?.cancel("New reload invoked")
cache.clear()
isFlawless = true
if (TestUtil.isInTest) return
- job = Firmament.coroutineScope.launch {
- val items = repository.items?.items ?: return@launch
- items.values.forEach {
- it.asItemStack() // Rebuild cache
- }
+ val newScope =
+ CoroutineScope(
+ Firmament.coroutineScope.coroutineContext +
+ SupervisorJob(Firmament.globalJob) +
+ Dispatchers.Default.limitedParallelism(
+ (Runtime.getRuntime().availableProcessors() / 4).coerceAtLeast(1)
+ )
+ )
+ val items = repository.items?.items
+ newScope.launch {
+ val items = items ?: return@launch
+ items.values.chunked(500).map { chunk ->
+ async {
+ chunk.forEach {
+ it.asItemStack() // Rebuild cache
+ }
+ }
+ }.awaitAll()
}
+ itemRecacheScope = newScope
}
fun coinItem(coinAmount: Int): ItemStack {
diff --git a/src/main/kotlin/repo/MiningRepoData.kt b/src/main/kotlin/repo/MiningRepoData.kt
index e40292d..e96a241 100644
--- a/src/main/kotlin/repo/MiningRepoData.kt
+++ b/src/main/kotlin/repo/MiningRepoData.kt
@@ -81,6 +81,7 @@ class MiningRepoData : IReloadable {
) {
@Transient
val dropItem = baseDrop?.let(::SBItemStack)
+ @OptIn(ExpensiveItemCacheApi::class)
private val labeledStack by lazy {
dropItem?.asCopiedItemStack()?.also(::markItemStack)
}
@@ -110,6 +111,7 @@ class MiningRepoData : IReloadable {
fun isActiveIn(location: SkyBlockIsland) = onlyIn == null || location in onlyIn
+ @OptIn(ExpensiveItemCacheApi::class)
private fun convertToModernBlock(): Block? {
// TODO: this should be in a shared util, really
val newCompound = ItemCache.convert189ToModern(NbtCompound().apply {
diff --git a/src/main/kotlin/repo/ModernOverlaysData.kt b/src/main/kotlin/repo/ModernOverlaysData.kt
new file mode 100644
index 0000000..543b800
--- /dev/null
+++ b/src/main/kotlin/repo/ModernOverlaysData.kt
@@ -0,0 +1,41 @@
+package moe.nea.firmament.repo
+
+import io.github.moulberry.repo.IReloadable
+import io.github.moulberry.repo.NEURepository
+import java.nio.file.Path
+import kotlin.io.path.extension
+import kotlin.io.path.isDirectory
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.nameWithoutExtension
+import moe.nea.firmament.util.SkyblockId
+
+// TODO: move this over to the repo parser
+class ModernOverlaysData : IReloadable {
+ data class OverlayFile(
+ val version: Int,
+ val path: Path,
+ )
+
+ var overlays: Map<SkyblockId, List<OverlayFile>> = mapOf()
+ override fun reload(repo: NEURepository) {
+ val items = mutableMapOf<SkyblockId, MutableList<OverlayFile>>()
+ repo.baseFolder.resolve("itemsOverlay")
+ .takeIf { it.isDirectory() }
+ ?.listDirectoryEntries()
+ ?.forEach { versionFolder ->
+ val version = versionFolder.fileName.toString().toIntOrNull() ?: return@forEach
+ versionFolder.listDirectoryEntries()
+ .forEach { item ->
+ if (item.extension != "snbt") return@forEach
+ val itemId = item.nameWithoutExtension
+ items.getOrPut(SkyblockId(itemId)) { mutableListOf() }.add(OverlayFile(version, item))
+ }
+ }
+ this.overlays = items
+ }
+
+ fun getOverlayFiles(skyblockId: SkyblockId) = overlays[skyblockId] ?: listOf()
+ fun getMostModernReadableOverlay(skyblockId: SkyblockId, version: Int) = getOverlayFiles(skyblockId)
+ .filter { it.version <= version }
+ .maxByOrNull { it.version }
+}
diff --git a/src/main/kotlin/repo/RepoDownloadManager.kt b/src/main/kotlin/repo/RepoDownloadManager.kt
index 3efd83b..888248d 100644
--- a/src/main/kotlin/repo/RepoDownloadManager.kt
+++ b/src/main/kotlin/repo/RepoDownloadManager.kt
@@ -1,5 +1,3 @@
-
-
package moe.nea.firmament.repo
import io.ktor.client.call.body
@@ -28,101 +26,102 @@ import moe.nea.firmament.util.iterate
object RepoDownloadManager {
- val repoSavedLocation = Firmament.DATA_DIR.resolve("repo-extracted")
- val repoMetadataLocation = Firmament.DATA_DIR.resolve("loaded-repo-sha.txt")
-
- private fun loadSavedVersionHash(): String? =
- if (repoSavedLocation.exists()) {
- if (repoMetadataLocation.exists()) {
- try {
- repoMetadataLocation.readText().trim()
- } catch (e: IOException) {
- null
- }
- } else {
- null
- }
- } else null
-
- private fun saveVersionHash(versionHash: String) {
- latestSavedVersionHash = versionHash
- repoMetadataLocation.writeText(versionHash)
- }
-
- var latestSavedVersionHash: String? = loadSavedVersionHash()
- private set
-
- @Serializable
- private class GithubCommitsResponse(val sha: String)
-
- private suspend fun requestLatestGithubSha(): String? {
- if (RepoManager.Config.branch == "prerelease") {
- RepoManager.Config.branch = "master"
- }
- val response =
- Firmament.httpClient.get("https://api.github.com/repos/${RepoManager.Config.username}/${RepoManager.Config.reponame}/commits/${RepoManager.Config.branch}")
- if (response.status.value != 200) {
- return null
- }
- return response.body<GithubCommitsResponse>().sha
- }
-
- private suspend fun downloadGithubArchive(url: String): Path = withContext(IO) {
- val response = Firmament.httpClient.get(url)
- val targetFile = Files.createTempFile("firmament-repo", ".zip")
- val outputChannel = Files.newByteChannel(targetFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE)
- response.bodyAsChannel().copyTo(outputChannel)
- targetFile
- }
-
- /**
- * Downloads the latest repository from github, setting [latestSavedVersionHash].
- * @return true, if an update was performed, false, otherwise (no update needed, or wasn't able to complete update)
- */
- suspend fun downloadUpdate(force: Boolean): Boolean = withContext(CoroutineName("Repo Update Check")) {
- val latestSha = requestLatestGithubSha()
- if (latestSha == null) {
- logger.warn("Could not request github API to retrieve latest REPO sha.")
- return@withContext false
- }
- val currentSha = loadSavedVersionHash()
- if (latestSha != currentSha || force) {
- val requestUrl =
- "https://github.com/${RepoManager.Config.username}/${RepoManager.Config.reponame}/archive/$latestSha.zip"
- logger.info("Planning to upgrade repository from $currentSha to $latestSha from $requestUrl")
- val zipFile = downloadGithubArchive(requestUrl)
- logger.info("Download repository zip file to $zipFile. Deleting old repository")
- withContext(IO) { repoSavedLocation.toFile().deleteRecursively() }
- logger.info("Extracting new repository")
- withContext(IO) { extractNewRepository(zipFile) }
- logger.info("Repository loaded on disk.")
- saveVersionHash(latestSha)
- return@withContext true
- } else {
- logger.debug("Repository on latest sha $currentSha. Not performing update")
- return@withContext false
- }
- }
-
- private fun extractNewRepository(zipFile: Path) {
- repoSavedLocation.createDirectories()
- ZipInputStream(zipFile.inputStream()).use { cis ->
- while (true) {
- val entry = cis.nextEntry ?: break
- if (entry.isDirectory) continue
- val extractedLocation =
- repoSavedLocation.resolve(
- entry.name.substringAfter('/', missingDelimiterValue = "")
- )
- if (repoSavedLocation !in extractedLocation.iterate { it.parent }) {
- logger.error("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.")
- throw RuntimeException("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.")
- }
- extractedLocation.parent.createDirectories()
- extractedLocation.outputStream().use { cis.copyTo(it) }
- }
- }
- }
+ val repoSavedLocation = Firmament.DATA_DIR.resolve("repo-extracted")
+ val repoMetadataLocation = Firmament.DATA_DIR.resolve("loaded-repo-sha.txt")
+
+ private fun loadSavedVersionHash(): String? =
+ if (repoSavedLocation.exists()) {
+ if (repoMetadataLocation.exists()) {
+ try {
+ repoMetadataLocation.readText().trim()
+ } catch (e: IOException) {
+ null
+ }
+ } else {
+ null
+ }
+ } else null
+
+ private fun saveVersionHash(versionHash: String) {
+ latestSavedVersionHash = versionHash
+ repoMetadataLocation.writeText(versionHash)
+ }
+
+ var latestSavedVersionHash: String? = loadSavedVersionHash()
+ private set
+
+ @Serializable
+ private class GithubCommitsResponse(val sha: String)
+
+ private suspend fun requestLatestGithubSha(branchOverride: String?): String? {
+ if (RepoManager.Config.branch == "prerelease") {
+ RepoManager.Config.branch = "master"
+ }
+ val response =
+ Firmament.httpClient.get("https://api.github.com/repos/${RepoManager.Config.username}/${RepoManager.Config.reponame}/commits/${branchOverride ?: RepoManager.Config.branch}")
+ if (response.status.value != 200) {
+ return null
+ }
+ return response.body<GithubCommitsResponse>().sha
+ }
+
+ private suspend fun downloadGithubArchive(url: String): Path = withContext(IO) {
+ val response = Firmament.httpClient.get(url)
+ val targetFile = Files.createTempFile("firmament-repo", ".zip")
+ val outputChannel = Files.newByteChannel(targetFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE)
+ response.bodyAsChannel().copyTo(outputChannel)
+ targetFile
+ }
+
+ /**
+ * Downloads the latest repository from github, setting [latestSavedVersionHash].
+ * @return true, if an update was performed, false, otherwise (no update needed, or wasn't able to complete update)
+ */
+ suspend fun downloadUpdate(force: Boolean, branch: String? = null): Boolean =
+ withContext(CoroutineName("Repo Update Check")) {
+ val latestSha = requestLatestGithubSha(branch)
+ if (latestSha == null) {
+ logger.warn("Could not request github API to retrieve latest REPO sha.")
+ return@withContext false
+ }
+ val currentSha = loadSavedVersionHash()
+ if (latestSha != currentSha || force) {
+ val requestUrl =
+ "https://github.com/${RepoManager.Config.username}/${RepoManager.Config.reponame}/archive/$latestSha.zip"
+ logger.info("Planning to upgrade repository from $currentSha to $latestSha from $requestUrl")
+ val zipFile = downloadGithubArchive(requestUrl)
+ logger.info("Download repository zip file to $zipFile. Deleting old repository")
+ withContext(IO) { repoSavedLocation.toFile().deleteRecursively() }
+ logger.info("Extracting new repository")
+ withContext(IO) { extractNewRepository(zipFile) }
+ logger.info("Repository loaded on disk.")
+ saveVersionHash(latestSha)
+ return@withContext true
+ } else {
+ logger.debug("Repository on latest sha $currentSha. Not performing update")
+ return@withContext false
+ }
+ }
+
+ private fun extractNewRepository(zipFile: Path) {
+ repoSavedLocation.createDirectories()
+ ZipInputStream(zipFile.inputStream()).use { cis ->
+ while (true) {
+ val entry = cis.nextEntry ?: break
+ if (entry.isDirectory) continue
+ val extractedLocation =
+ repoSavedLocation.resolve(
+ entry.name.substringAfter('/', missingDelimiterValue = "")
+ )
+ if (repoSavedLocation !in extractedLocation.iterate { it.parent }) {
+ logger.error("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.")
+ throw RuntimeException("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.")
+ }
+ extractedLocation.parent.createDirectories()
+ extractedLocation.outputStream().use { cis.copyTo(it) }
+ }
+ }
+ }
}
diff --git a/src/main/kotlin/repo/RepoManager.kt b/src/main/kotlin/repo/RepoManager.kt
index e50a131..df89092 100644
--- a/src/main/kotlin/repo/RepoManager.kt
+++ b/src/main/kotlin/repo/RepoManager.kt
@@ -11,6 +11,7 @@ import kotlinx.coroutines.launch
import net.minecraft.client.MinecraftClient
import net.minecraft.network.packet.s2c.play.SynchronizeRecipesS2CPacket
import net.minecraft.recipe.display.CuttingRecipeDisplay
+import net.minecraft.util.StringIdentifiable
import moe.nea.firmament.Firmament
import moe.nea.firmament.Firmament.logger
import moe.nea.firmament.events.ReloadRegistrationEvent
@@ -46,6 +47,16 @@ object RepoManager {
}
val alwaysSuperCraft by toggle("enable-super-craft") { true }
var warnForMissingItemListMod by toggle("warn-for-missing-item-list-mod") { true }
+ val perfectRenders by choice("perfect-renders") { PerfectRender.RENDER }
+ }
+
+ enum class PerfectRender(val label: String) : StringIdentifiable {
+ NOTHING("nothing"),
+ RENDER("render"),
+ RENDER_AND_TEXT("text"),
+ ;
+
+ override fun asString(): String? = label
}
val currentDownloadedSha by RepoDownloadManager::latestSavedVersionHash
@@ -55,9 +66,11 @@ object RepoManager {
val essenceRecipeProvider = EssenceRecipeProvider()
val recipeCache = BetterRepoRecipeCache(essenceRecipeProvider, ReforgeStore)
val miningData = MiningRepoData()
+ val overlayData = ModernOverlaysData()
fun makeNEURepository(path: Path): NEURepository {
return NEURepository.of(path).apply {
+ registerReloadListener(overlayData)
registerReloadListener(ItemCache)
registerReloadListener(RepoItemTypeCache)
registerReloadListener(ExpLadders)
@@ -102,6 +115,13 @@ object RepoManager {
fun getNEUItem(skyblockId: SkyblockId): NEUItem? = neuRepo.items.getItemBySkyblockId(skyblockId.neuItem)
+ fun downloadOverridenBranch(branch: String) {
+ Firmament.coroutineScope.launch {
+ RepoDownloadManager.downloadUpdate(true, branch)
+ reload()
+ }
+ }
+
fun launchAsyncUpdate(force: Boolean = false) {
Firmament.coroutineScope.launch {
RepoDownloadManager.downloadUpdate(force)
@@ -128,8 +148,10 @@ object RepoManager {
} catch (exc: NEURepositoryException) {
ErrorUtil.softError("Failed to reload repository", exc)
MC.sendChat(
- tr("firmament.repo.reloadfail",
- "Failed to reload repository. This will result in some mod features not working.")
+ tr(
+ "firmament.repo.reloadfail",
+ "Failed to reload repository. This will result in some mod features not working."
+ )
)
}
}
diff --git a/src/main/kotlin/repo/RepoModResourcePack.kt b/src/main/kotlin/repo/RepoModResourcePack.kt
index 2fdf710..4fec14a 100644
--- a/src/main/kotlin/repo/RepoModResourcePack.kt
+++ b/src/main/kotlin/repo/RepoModResourcePack.kt
@@ -24,7 +24,7 @@ import net.minecraft.resource.metadata.ResourceMetadata
import net.minecraft.resource.metadata.ResourceMetadataSerializer
import net.minecraft.text.Text
import net.minecraft.util.Identifier
-import net.minecraft.util.PathUtil
+import net.minecraft.util.path.PathUtil
import moe.nea.firmament.Firmament
class RepoModResourcePack(val basePath: Path) : ModResourcePack {
diff --git a/src/main/kotlin/repo/SBItemStack.kt b/src/main/kotlin/repo/SBItemStack.kt
index 3690866..01d1c4d 100644
--- a/src/main/kotlin/repo/SBItemStack.kt
+++ b/src/main/kotlin/repo/SBItemStack.kt
@@ -225,14 +225,21 @@ data class SBItemStack constructor(
Text.literal(
buffKind.prefix + formattedAmount +
statFormatting.postFix +
- buffKind.postFix + " ")
- .withColor(buffKind.color)))
+ buffKind.postFix + " "
+ )
+ .withColor(buffKind.color)
+ )
+ )
}
fun formatValue() =
- Text.literal(FirmFormatters.formatCommas(valueNum ?: 0.0,
- 1,
- includeSign = true) + statFormatting.postFix + " ")
+ Text.literal(
+ FirmFormatters.formatCommas(
+ valueNum ?: 0.0,
+ 1,
+ includeSign = true
+ ) + statFormatting.postFix + " "
+ )
.setStyle(Style.EMPTY.withColor(statFormatting.color))
val statFormatting = formattingOverrides[statName] ?: StatFormatting("", Formatting.GREEN)
@@ -256,7 +263,7 @@ data class SBItemStack constructor(
return segments.joinToString(" ") { it.replaceFirstChar { it.uppercaseChar() } }
}
- private fun parseStatLine(line: Text): StatLine? {
+ fun parseStatLine(line: Text): StatLine? {
val sibs = line.siblings
val stat = sibs.firstOrNull() ?: return null
if (stat.style.color != TextColor.fromFormatting(Formatting.GRAY)) return null
@@ -346,7 +353,9 @@ data class SBItemStack constructor(
}
// TODO: avoid instantiating the item stack here
+ @ExpensiveItemCacheApi
val itemType: ItemType? get() = ItemType.fromItemStack(asImmutableItemStack())
+ @ExpensiveItemCacheApi
val rarity: Rarity? get() = Rarity.fromItem(asImmutableItemStack())
private var itemStack_: ItemStack? = null
@@ -357,6 +366,7 @@ data class SBItemStack constructor(
group("power").toInt()
} ?: 0
+ @ExpensiveItemCacheApi
private val itemStack: ItemStack
get() {
val itemStack = itemStack_ ?: run {
@@ -413,19 +423,35 @@ data class SBItemStack constructor(
.append(starString(stars))
val isDungeon = ItemType.fromItemStack(itemStack)?.isDungeon ?: true
val truncatedStarCount = if (isDungeon) minOf(5, stars) else stars
- appendEnhancedStats(itemStack,
- baseStats
- .filter { it.statFormatting.isStarAffected }
- .associate {
- it.statName to ((it.valueNum ?: 0.0) * (truncatedStarCount * 0.02))
- },
- BuffKind.STAR_BUFF)
+ appendEnhancedStats(
+ itemStack,
+ baseStats
+ .filter { it.statFormatting.isStarAffected }
+ .associate {
+ it.statName to ((it.valueNum ?: 0.0) * (truncatedStarCount * 0.02))
+ },
+ BuffKind.STAR_BUFF
+ )
+ }
+
+ fun isWarm(): Boolean {
+ if (itemStack_ != null) return true
+ if (ItemCache.hasCacheFor(skyblockId)) return true
+ return false
+ }
+
+ @OptIn(ExpensiveItemCacheApi::class)
+ fun asLazyImmutableItemStack(): ItemStack? {
+ if (isWarm()) return asImmutableItemStack()
+ return null
}
- fun asImmutableItemStack(): ItemStack {
+ @ExpensiveItemCacheApi
+ fun asImmutableItemStack(): ItemStack { // TODO: add a "or fallback to painting" option to asLazyImmutableItemStack to be used in more places.
return itemStack
}
+ @ExpensiveItemCacheApi
fun asCopiedItemStack(): ItemStack {
return itemStack.copy()
}
diff --git a/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt b/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt
index 9a1aea5..3774f26 100644
--- a/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt
+++ b/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt
@@ -9,11 +9,13 @@ import net.minecraft.util.Identifier
import moe.nea.firmament.repo.SBItemStack
interface GenericRecipeRenderer<T : NEURecipe> {
- fun render(recipe: T, bounds: Rectangle, layouter: RecipeLayouter)
+ fun render(recipe: T, bounds: Rectangle, layouter: RecipeLayouter, mainItem: SBItemStack?)
fun getInputs(recipe: T): Collection<SBItemStack>
fun getOutputs(recipe: T): Collection<SBItemStack>
val icon: ItemStack
val title: Text
val identifier: Identifier
fun findAllRecipes(neuRepository: NEURepository): Iterable<T>
+ val displayHeight: Int get() = 66
+ val typ: Class<T>
}
diff --git a/src/main/kotlin/repo/recipes/RecipeLayouter.kt b/src/main/kotlin/repo/recipes/RecipeLayouter.kt
index 109bff5..ed0dca2 100644
--- a/src/main/kotlin/repo/recipes/RecipeLayouter.kt
+++ b/src/main/kotlin/repo/recipes/RecipeLayouter.kt
@@ -1,6 +1,8 @@
package moe.nea.firmament.repo.recipes
import io.github.notenoughupdates.moulconfig.gui.GuiComponent
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
import net.minecraft.text.Text
import moe.nea.firmament.repo.SBItemStack
@@ -21,13 +23,16 @@ interface RecipeLayouter {
slotKind: SlotKind,
)
+ fun createTooltip(rectangle: Rectangle, label: Text)
+
fun createLabel(
x: Int, y: Int,
text: Text
)
- fun createArrow(x: Int, y: Int)
+ fun createArrow(x: Int, y: Int): Rectangle
fun createMoulConfig(x: Int, y: Int, w: Int, h: Int, component: GuiComponent)
+ fun createFire(ingredientsCenter: Point, animationTicks: Int)
}
diff --git a/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt
index 679aec8..e38380c 100644
--- a/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt
+++ b/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt
@@ -12,17 +12,24 @@ import moe.nea.firmament.Firmament
import moe.nea.firmament.repo.SBItemStack
import moe.nea.firmament.util.tr
-class SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> {
- override fun render(recipe: NEUCraftingRecipe, bounds: Rectangle, layouter: RecipeLayouter) {
+object SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> {
+ override fun render(
+ recipe: NEUCraftingRecipe,
+ bounds: Rectangle,
+ layouter: RecipeLayouter,
+ mainItem: SBItemStack?,
+ ) {
val point = Point(bounds.centerX - 58, bounds.centerY - 27)
layouter.createArrow(point.x + 60, point.y + 18)
for (i in 0 until 3) {
for (j in 0 until 3) {
val item = recipe.inputs[i + j * 3]
- layouter.createItemSlot(point.x + 1 + i * 18,
- point.y + 1 + j * 18,
- SBItemStack(item),
- RecipeLayouter.SlotKind.SMALL_INPUT)
+ layouter.createItemSlot(
+ point.x + 1 + i * 18,
+ point.y + 1 + j * 18,
+ SBItemStack(item),
+ RecipeLayouter.SlotKind.SMALL_INPUT
+ )
}
}
layouter.createItemSlot(
@@ -32,6 +39,9 @@ class SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> {
)
}
+ override val typ: Class<NEUCraftingRecipe>
+ get() = NEUCraftingRecipe::class.java
+
override fun getInputs(recipe: NEUCraftingRecipe): Collection<SBItemStack> {
return recipe.allInputs.mapNotNull { SBItemStack(it) }
}
@@ -45,6 +55,6 @@ class SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> {
}
override val icon: ItemStack = ItemStack(Blocks.CRAFTING_TABLE)
- override val title: Text = tr("firmament.category.crafting", "SkyBlock Crafting") // TODO: fix tr not being included in jars
+ override val title: Text = tr("firmament.category.crafting", "SkyBlock Crafting")
override val identifier: Identifier = Firmament.identifier("crafting_recipe")
}
diff --git a/src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt
new file mode 100644
index 0000000..d358e6a
--- /dev/null
+++ b/src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt
@@ -0,0 +1,76 @@
+package moe.nea.firmament.repo.recipes
+
+import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.data.NEUForgeRecipe
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import net.minecraft.item.ItemStack
+import net.minecraft.text.Text
+import net.minecraft.util.Identifier
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.repo.EssenceRecipeProvider
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.tr
+
+object SBEssenceUpgradeRecipeRenderer : GenericRecipeRenderer<EssenceRecipeProvider.EssenceUpgradeRecipe> {
+ override fun render(
+ recipe: EssenceRecipeProvider.EssenceUpgradeRecipe,
+ bounds: Rectangle,
+ layouter: RecipeLayouter,
+ mainItem: SBItemStack?
+ ) {
+ val sourceItem = mainItem ?: SBItemStack(recipe.itemId)
+ layouter.createItemSlot(
+ bounds.minX + 12,
+ bounds.centerY - 8 - 18 / 2,
+ sourceItem.copy(stars = recipe.starCountAfter - 1),
+ RecipeLayouter.SlotKind.SMALL_INPUT
+ )
+ layouter.createItemSlot(
+ bounds.minX + 12, bounds.centerY - 8 + 18 / 2,
+ SBItemStack(recipe.essenceIngredient),
+ RecipeLayouter.SlotKind.SMALL_INPUT
+ )
+ layouter.createItemSlot(
+ bounds.maxX - 12 - 16, bounds.centerY - 8,
+ sourceItem.copy(stars = recipe.starCountAfter),
+ RecipeLayouter.SlotKind.SMALL_OUTPUT
+ )
+ val extraItems = recipe.extraItems
+ layouter.createArrow(
+ bounds.centerX - 24 / 2,
+ if (extraItems.isEmpty()) bounds.centerY - 17 / 2
+ else bounds.centerY + 18 / 2
+ )
+ for ((index, item) in extraItems.withIndex()) {
+ layouter.createItemSlot(
+ bounds.centerX - extraItems.size * 16 / 2 - 2 / 2 + index * 18,
+ bounds.centerY - 18 / 2,
+ SBItemStack(item),
+ RecipeLayouter.SlotKind.SMALL_INPUT,
+ )
+ }
+ }
+
+ override fun getInputs(recipe: EssenceRecipeProvider.EssenceUpgradeRecipe): Collection<SBItemStack> {
+ return recipe.allInputs.mapNotNull { SBItemStack(it) }
+ }
+
+ override fun getOutputs(recipe: EssenceRecipeProvider.EssenceUpgradeRecipe): Collection<SBItemStack> {
+ return listOfNotNull(SBItemStack(recipe.itemId))
+ }
+
+ @OptIn(ExpensiveItemCacheApi::class)
+ override val icon: ItemStack get() = SBItemStack(SkyblockId("ESSENCE_WITHER")).asImmutableItemStack()
+ override val title: Text = tr("firmament.category.essence", "Essence Upgrades")
+ override val identifier: Identifier = Firmament.identifier("essence_upgrade")
+ override fun findAllRecipes(neuRepository: NEURepository): Iterable<EssenceRecipeProvider.EssenceUpgradeRecipe> {
+ return RepoManager.essenceRecipeProvider.recipes
+ }
+
+ override val typ: Class<EssenceRecipeProvider.EssenceUpgradeRecipe>
+ get() = EssenceRecipeProvider.EssenceUpgradeRecipe::class.java
+}
diff --git a/src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt
new file mode 100644
index 0000000..9fdb756
--- /dev/null
+++ b/src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt
@@ -0,0 +1,83 @@
+package moe.nea.firmament.repo.recipes
+
+import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.data.NEUCraftingRecipe
+import io.github.moulberry.repo.data.NEUForgeRecipe
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import kotlin.math.cos
+import kotlin.math.sin
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.block.Blocks
+import net.minecraft.item.ItemStack
+import net.minecraft.text.Text
+import net.minecraft.util.Identifier
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.util.tr
+
+object SBForgeRecipeRenderer : GenericRecipeRenderer<NEUForgeRecipe> {
+ override fun render(
+ recipe: NEUForgeRecipe,
+ bounds: Rectangle,
+ layouter: RecipeLayouter,
+ mainItem: SBItemStack?,
+ ) {
+ val arrow = layouter.createArrow(bounds.minX + 90, bounds.minY + 54 - 18 / 2)
+ layouter.createTooltip(
+ arrow,
+ Text.stringifiedTranslatable(
+ "firmament.recipe.forge.time",
+ recipe.duration.seconds
+ )
+ )
+
+ val ingredientsCenter = Point(bounds.minX + 49 - 8, bounds.minY + 54 - 8)
+ layouter.createFire(ingredientsCenter, 25)
+ val count = recipe.inputs.size
+ if (count == 1) {
+ layouter.createItemSlot(
+ ingredientsCenter.x, ingredientsCenter.y - 18,
+ SBItemStack(recipe.inputs.single()),
+ RecipeLayouter.SlotKind.SMALL_INPUT,
+ )
+ } else {
+ recipe.inputs.forEachIndexed { idx, ingredient ->
+ val rad = Math.PI * 2 * idx / count
+ layouter.createItemSlot(
+ (ingredientsCenter.x + cos(rad) * 30).toInt(), (ingredientsCenter.y + sin(rad) * 30).toInt(),
+ SBItemStack(ingredient),
+ RecipeLayouter.SlotKind.SMALL_INPUT,
+ )
+ }
+ }
+ layouter.createItemSlot(
+ bounds.minX + 124, bounds.minY + 46,
+ SBItemStack(recipe.outputStack),
+ RecipeLayouter.SlotKind.BIG_OUTPUT
+ )
+ }
+
+ override val displayHeight: Int
+ get() = 104
+
+ override fun getInputs(recipe: NEUForgeRecipe): Collection<SBItemStack> {
+ return recipe.inputs.mapNotNull { SBItemStack(it) }
+ }
+
+ override fun getOutputs(recipe: NEUForgeRecipe): Collection<SBItemStack> {
+ return listOfNotNull(SBItemStack(recipe.outputStack))
+ }
+
+ override val icon: ItemStack = ItemStack(Blocks.ANVIL)
+ override val title: Text = tr("firmament.category.forge", "Forge Recipes")
+ override val identifier: Identifier = Firmament.identifier("forge_recipe")
+
+ override fun findAllRecipes(neuRepository: NEURepository): Iterable<NEUForgeRecipe> {
+ // TODO: theres gotta be an index for these tbh.
+ return neuRepository.items.items.values.flatMap { it.recipes }.filterIsInstance<NEUForgeRecipe>()
+ }
+
+ override val typ: Class<NEUForgeRecipe>
+ get() = NEUForgeRecipe::class.java
+}
diff --git a/src/main/kotlin/util/Base64Util.kt b/src/main/kotlin/util/Base64Util.kt
index 44bcdfd..c39c601 100644
--- a/src/main/kotlin/util/Base64Util.kt
+++ b/src/main/kotlin/util/Base64Util.kt
@@ -1,7 +1,14 @@
package moe.nea.firmament.util
+import java.util.Base64
+
object Base64Util {
+ fun decodeString(str: String): String {
+ return Base64.getDecoder().decode(str.padToValidBase64())
+ .decodeToString()
+ }
+
fun String.padToValidBase64(): String {
val align = this.length % 4
if (align == 0) return this
diff --git a/src/main/kotlin/util/BazaarPriceStrategy.kt b/src/main/kotlin/util/BazaarPriceStrategy.kt
index 002eedb..13b6d95 100644
--- a/src/main/kotlin/util/BazaarPriceStrategy.kt
+++ b/src/main/kotlin/util/BazaarPriceStrategy.kt
@@ -9,7 +9,7 @@ enum class BazaarPriceStrategy {
NPC_SELL;
fun getSellPrice(skyblockId: SkyblockId): Double {
- val bazaarEntry = HypixelStaticData.bazaarData[skyblockId] ?: return 0.0
+ val bazaarEntry = HypixelStaticData.bazaarData[skyblockId.asBazaarStock] ?: return 0.0
return when (this) {
BUY_ORDER -> bazaarEntry.quickStatus.sellPrice
SELL_ORDER -> bazaarEntry.quickStatus.buyPrice
diff --git a/src/main/kotlin/util/ErrorUtil.kt b/src/main/kotlin/util/ErrorUtil.kt
index 190381d..3db4ecd 100644
--- a/src/main/kotlin/util/ErrorUtil.kt
+++ b/src/main/kotlin/util/ErrorUtil.kt
@@ -29,15 +29,31 @@ object ErrorUtil {
inline fun softError(message: String, exception: Throwable) {
if (aggressiveErrors) throw IllegalStateException(message, exception)
- else Firmament.logger.error(message, exception)
+ else logError(message, exception)
+ }
+
+ fun logError(message: String, exception: Throwable) {
+ Firmament.logger.error(message, exception)
+ }
+ fun logError(message: String) {
+ Firmament.logger.error(message)
}
inline fun softError(message: String) {
if (aggressiveErrors) error(message)
- else Firmament.logger.error(message)
+ else logError(message)
+ }
+
+ fun <T> Result<T>.intoCatch(message: String): Catch<T> {
+ return this.map { Catch.succeed(it) }.getOrElse {
+ softError(message, it)
+ Catch.fail(it)
+ }
}
class Catch<T> private constructor(val value: T?, val exc: Throwable?) {
+ fun orNull(): T? = value
+
inline fun or(block: (exc: Throwable) -> T): T {
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
@@ -73,4 +89,9 @@ object ErrorUtil {
return nullable
}
+ fun softUserError(string: String) {
+ if (TestUtil.isInTest)
+ error(string)
+ MC.sendChat(tr("firmament.usererror", "Firmament encountered a user caused error: $string"))
+ }
}
diff --git a/src/main/kotlin/util/FirmFormatters.kt b/src/main/kotlin/util/FirmFormatters.kt
index acb7102..03dafc5 100644
--- a/src/main/kotlin/util/FirmFormatters.kt
+++ b/src/main/kotlin/util/FirmFormatters.kt
@@ -13,6 +13,7 @@ import kotlin.math.roundToInt
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import net.minecraft.text.Text
+import net.minecraft.util.math.BlockPos
object FirmFormatters {
@@ -131,4 +132,11 @@ object FirmFormatters {
return if (boolean == trueIsGood) text.lime() else text.red()
}
+ fun formatPosition(position: BlockPos): Text {
+ return Text.literal("x: ${position.x}, y: ${position.y}, z: ${position.z}")
+ }
+
+ fun formatPercent(value: Double, decimals: Int = 1): String {
+ return "%.${decimals}f%%".format(value * 100)
+ }
}
diff --git a/src/main/kotlin/util/HoveredItemStack.kt b/src/main/kotlin/util/HoveredItemStack.kt
index a2e4ad2..526820a 100644
--- a/src/main/kotlin/util/HoveredItemStack.kt
+++ b/src/main/kotlin/util/HoveredItemStack.kt
@@ -24,4 +24,4 @@ class VanillaScreenProvider : HoveredItemStackProvider {
val HandledScreen<*>.focusedItemStack: ItemStack?
get() =
HoveredItemStackProvider.allValidInstances
- .firstNotNullOfOrNull { it.provideHoveredItemStack(this) }
+ .firstNotNullOfOrNull { it.provideHoveredItemStack(this)?.takeIf { !it.isEmpty } }
diff --git a/src/main/kotlin/util/LegacyTagWriter.kt b/src/main/kotlin/util/LegacyTagWriter.kt
new file mode 100644
index 0000000..9889b2c
--- /dev/null
+++ b/src/main/kotlin/util/LegacyTagWriter.kt
@@ -0,0 +1,103 @@
+package moe.nea.firmament.util
+
+import kotlinx.serialization.json.JsonPrimitive
+import net.minecraft.nbt.AbstractNbtList
+import net.minecraft.nbt.NbtByte
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.nbt.NbtDouble
+import net.minecraft.nbt.NbtElement
+import net.minecraft.nbt.NbtEnd
+import net.minecraft.nbt.NbtFloat
+import net.minecraft.nbt.NbtInt
+import net.minecraft.nbt.NbtLong
+import net.minecraft.nbt.NbtShort
+import net.minecraft.nbt.NbtString
+import moe.nea.firmament.util.mc.SNbtFormatter.Companion.SIMPLE_NAME
+
+class LegacyTagWriter(val compact: Boolean) {
+ companion object {
+ fun stringify(nbt: NbtElement, compact: Boolean): String {
+ return LegacyTagWriter(compact).also { it.writeElement(nbt) }
+ .stringWriter.toString()
+ }
+
+ fun NbtElement.toLegacyString(pretty: Boolean = false): String {
+ return stringify(this, !pretty)
+ }
+ }
+
+ val stringWriter = StringBuilder()
+ var indent = 0
+ fun newLine() {
+ if (compact) return
+ stringWriter.append('\n')
+ repeat(indent) {
+ stringWriter.append(" ")
+ }
+ }
+
+ fun writeElement(nbt: NbtElement) {
+ when (nbt) {
+ is NbtInt -> stringWriter.append(nbt.value.toString())
+ is NbtString -> stringWriter.append(escapeString(nbt.value))
+ is NbtFloat -> stringWriter.append(nbt.value).append('F')
+ is NbtDouble -> stringWriter.append(nbt.value).append('D')
+ is NbtByte -> stringWriter.append(nbt.value).append('B')
+ is NbtLong -> stringWriter.append(nbt.value).append('L')
+ is NbtShort -> stringWriter.append(nbt.value).append('S')
+ is NbtCompound -> writeCompound(nbt)
+ is NbtEnd -> {}
+ is AbstractNbtList -> writeArray(nbt)
+ }
+ }
+
+ fun writeArray(nbt: AbstractNbtList) {
+ stringWriter.append('[')
+ indent++
+ newLine()
+ nbt.forEachIndexed { index, element ->
+ writeName(index.toString())
+ writeElement(element)
+ if (index != nbt.size() - 1) {
+ stringWriter.append(',')
+ newLine()
+ }
+ }
+ indent--
+ if (nbt.size() != 0)
+ newLine()
+ stringWriter.append(']')
+ }
+
+ fun writeCompound(nbt: NbtCompound) {
+ stringWriter.append('{')
+ indent++
+ newLine()
+ val entries = nbt.entrySet().sortedBy { it.key }
+ entries.forEachIndexed { index, it ->
+ writeName(it.key)
+ writeElement(it.value)
+ if (index != entries.lastIndex) {
+ stringWriter.append(',')
+ newLine()
+ }
+ }
+ indent--
+ if (nbt.size != 0)
+ newLine()
+ stringWriter.append('}')
+ }
+
+ fun escapeString(string: String): String {
+ return JsonPrimitive(string).toString()
+ }
+
+ fun escapeName(key: String): String =
+ if (key.matches(SIMPLE_NAME)) key else escapeString(key)
+
+ fun writeName(key: String) {
+ stringWriter.append(escapeName(key))
+ stringWriter.append(':')
+ if (!compact) stringWriter.append(' ')
+ }
+}
diff --git a/src/main/kotlin/util/MC.kt b/src/main/kotlin/util/MC.kt
index c1a5e65..e85b119 100644
--- a/src/main/kotlin/util/MC.kt
+++ b/src/main/kotlin/util/MC.kt
@@ -1,7 +1,9 @@
package moe.nea.firmament.util
import io.github.moulberry.repo.data.Coordinate
+import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper
import java.util.concurrent.ConcurrentLinkedQueue
+import kotlin.jvm.optionals.getOrNull
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.hud.InGameHud
import net.minecraft.client.gui.screen.Screen
@@ -16,10 +18,14 @@ import net.minecraft.item.Item
import net.minecraft.item.ItemStack
import net.minecraft.network.packet.c2s.play.CommandExecutionC2SPacket
import net.minecraft.registry.BuiltinRegistries
+import net.minecraft.registry.Registry
+import net.minecraft.registry.RegistryKey
import net.minecraft.registry.RegistryKeys
import net.minecraft.registry.RegistryWrapper
import net.minecraft.resource.ReloadableResourceManagerImpl
import net.minecraft.text.Text
+import net.minecraft.util.Identifier
+import net.minecraft.util.Util
import net.minecraft.util.math.BlockPos
import net.minecraft.world.World
import moe.nea.firmament.events.TickEvent
@@ -99,7 +105,7 @@ object MC {
inline val soundManager get() = instance.soundManager
inline val player: ClientPlayerEntity? get() = TestUtil.unlessTesting { instance.player }
inline val camera: Entity? get() = instance.cameraEntity
- inline val stackInHand: ItemStack get() = player?.inventory?.mainHandStack ?: ItemStack.EMPTY
+ inline val stackInHand: ItemStack get() = player?.mainHandStack ?: ItemStack.EMPTY
inline val guiAtlasManager get() = instance.guiAtlasManager
inline val world: ClientWorld? get() = TestUtil.unlessTesting { instance.world }
inline val playerName: String? get() = player?.name?.unformattedString
@@ -120,6 +126,25 @@ object MC {
return field
}
private set
+
+ val currentMoulConfigContext
+ get() = (screen as? GuiComponentWrapper)?.context
+
+ fun openUrl(uri: String) {
+ Util.getOperatingSystem().open(uri)
+ }
+
+ fun <T> unsafeGetRegistryEntry(registry: RegistryKey<out Registry<T>>, identifier: Identifier) =
+ unsafeGetRegistryEntry(RegistryKey.of(registry, identifier))
+
+
+ fun <T> unsafeGetRegistryEntry(registryKey: RegistryKey<T>): T? {
+ return currentOrDefaultRegistries
+ .getOrThrow(registryKey.registryRef)
+ .getOptional(registryKey)
+ .getOrNull()
+ ?.value()
+ }
}
diff --git a/src/main/kotlin/util/MoulConfigUtils.kt b/src/main/kotlin/util/MoulConfigUtils.kt
index 362a4d9..51ff340 100644
--- a/src/main/kotlin/util/MoulConfigUtils.kt
+++ b/src/main/kotlin/util/MoulConfigUtils.kt
@@ -35,6 +35,21 @@ import moe.nea.firmament.gui.TickComponent
import moe.nea.firmament.util.render.isUntranslatedGuiDrawContext
object MoulConfigUtils {
+ @JvmStatic
+ fun main(args: Array<out String>) {
+ generateXSD(File("MoulConfig.xsd"), XMLUniverse.MOULCONFIG_XML_NS)
+ generateXSD(File("MoulConfig.Firmament.xsd"), firmUrl)
+ File("wrapper.xsd").writeText(
+ """
+<?xml version="1.0" encoding="UTF-8" ?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:import namespace="http://notenoughupdates.org/moulconfig" schemaLocation="MoulConfig.xsd"/>
+ <xs:import namespace="http://firmament.nea.moe/moulconfig" schemaLocation="MoulConfig.Firmament.xsd"/>
+</xs:schema>
+ """.trimIndent()
+ )
+ }
+
val firmUrl = "http://firmament.nea.moe/moulconfig"
val universe = XMLUniverse.getDefaultUniverse().also { uni ->
uni.registerMapper(java.awt.Color::class.java) {
@@ -81,9 +96,11 @@ object MoulConfigUtils {
override fun createInstance(context: XMLContext<*>, element: Element): FirmHoverComponent {
return FirmHoverComponent(
context.getChildFragment(element),
- context.getPropertyFromAttribute(element,
- QName("lines"),
- List::class.java) as Supplier<List<String>>,
+ context.getPropertyFromAttribute(
+ element,
+ QName("lines"),
+ List::class.java
+ ) as Supplier<List<String>>,
context.getPropertyFromAttribute(element, QName("delay"), Duration::class.java, 0.6.seconds),
)
}
@@ -179,10 +196,8 @@ object MoulConfigUtils {
uni.registerLoader(object : XMLGuiLoader.Basic<FixedComponent> {
override fun createInstance(context: XMLContext<*>, element: Element): FixedComponent {
return FixedComponent(
- context.getPropertyFromAttribute(element, QName("width"), Int::class.java)
- ?: error("Requires width specified"),
- context.getPropertyFromAttribute(element, QName("height"), Int::class.java)
- ?: error("Requires height specified"),
+ context.getPropertyFromAttribute(element, QName("width"), Int::class.java),
+ context.getPropertyFromAttribute(element, QName("height"), Int::class.java),
context.getChildFragment(element)
)
}
@@ -196,7 +211,7 @@ object MoulConfigUtils {
}
override fun getAttributeNames(): Map<String, Boolean> {
- return mapOf("width" to true, "height" to true)
+ return mapOf("width" to false, "height" to false)
}
})
}
@@ -210,29 +225,21 @@ object MoulConfigUtils {
generator.dumpToFile(file)
}
- @JvmStatic
- fun main(args: Array<out String>) {
- generateXSD(File("MoulConfig.xsd"), XMLUniverse.MOULCONFIG_XML_NS)
- generateXSD(File("MoulConfig.Firmament.xsd"), firmUrl)
- File("wrapper.xsd").writeText("""
-<?xml version="1.0" encoding="UTF-8" ?>
-<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
- <xs:import namespace="http://notenoughupdates.org/moulconfig" schemaLocation="MoulConfig.xsd"/>
- <xs:import namespace="http://firmament.nea.moe/moulconfig" schemaLocation="MoulConfig.Firmament.xsd"/>
-</xs:schema>
- """.trimIndent())
- }
-
- fun loadScreen(name: String, bindTo: Any, parent: Screen?): Screen {
- return object : GuiComponentWrapper(loadGui(name, bindTo)) {
+ fun wrapScreen(guiContext: GuiContext, parent: Screen?, onClose: () -> Unit = {}): Screen {
+ return object : GuiComponentWrapper(guiContext) {
override fun close() {
if (context.onBeforeClose() == CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE) {
client!!.setScreen(parent)
+ onClose()
}
}
}
}
+ fun loadScreen(name: String, bindTo: Any, parent: Screen?): Screen {
+ return wrapScreen(loadGui(name, bindTo), parent)
+ }
+
// TODO: move this utility into moulconfig (also rework guicontext into an interface so i can make this mesh better into vanilla)
fun GuiContext.adopt(element: GuiComponent) = element.foldRecursive(Unit, { comp, unit -> comp.context = this })
@@ -288,12 +295,14 @@ object MoulConfigUtils {
assert(drawContext?.isUntranslatedGuiDrawContext() != false)
val context = drawContext?.let(::ModernRenderContext)
?: IMinecraft.instance.provideTopLevelRenderContext()
- val immContext = GuiImmediateContext(context,
- 0, 0, 0, 0,
- mouseX, mouseY,
- mouseX, mouseY,
- mouseX.toFloat(),
- mouseY.toFloat())
+ val immContext = GuiImmediateContext(
+ context,
+ 0, 0, 0, 0,
+ mouseX, mouseY,
+ mouseX, mouseY,
+ mouseX.toFloat(),
+ mouseY.toFloat()
+ )
return immContext
}
diff --git a/src/main/kotlin/util/SkyblockId.kt b/src/main/kotlin/util/SkyblockId.kt
index a31255c..b4d583a 100644
--- a/src/main/kotlin/util/SkyblockId.kt
+++ b/src/main/kotlin/util/SkyblockId.kt
@@ -6,6 +6,11 @@ import com.mojang.serialization.Codec
import io.github.moulberry.repo.data.NEUIngredient
import io.github.moulberry.repo.data.NEUItem
import io.github.moulberry.repo.data.Rarity
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatterBuilder
+import java.time.format.SignStyle
+import java.time.temporal.ChronoField
import java.util.Optional
import java.util.UUID
import kotlinx.serialization.Serializable
@@ -21,28 +26,32 @@ import net.minecraft.network.RegistryByteBuf
import net.minecraft.network.codec.PacketCodec
import net.minecraft.network.codec.PacketCodecs
import net.minecraft.util.Identifier
+import moe.nea.firmament.repo.ExpLadders
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.ItemCache.asItemStack
+import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.repo.set
import moe.nea.firmament.util.collections.WeakCache
import moe.nea.firmament.util.json.DashlessUUIDSerializer
/**
* A SkyBlock item id, as used by the NEU repo.
- * This is not exactly the format used by HyPixel, but is mostly the same.
- * Usually this id splits an id used by HyPixel into more sub items. For example `PET` becomes `$PET_ID;$PET_RARITY`,
+ * This is not exactly the format used by Hypixel, but is mostly the same.
+ * Usually this id splits an id used by Hypixel into more sub items. For example `PET` becomes `$PET_ID;$PET_RARITY`,
* with those values extracted from other metadata.
*/
@JvmInline
@Serializable
value class SkyblockId(val neuItem: String) : Comparable<SkyblockId> {
val identifier
- get() = Identifier.of("skyblockitem",
- neuItem.lowercase().replace(";", "__")
- .replace(":", "___")
- .replace(illlegalPathRegex) {
- it.value.toCharArray()
- .joinToString("") { "__" + it.code.toString(16).padStart(4, '0') }
- })
+ get() = Identifier.of(
+ "skyblockitem",
+ neuItem.lowercase().replace(";", "__")
+ .replace(":", "___")
+ .replace(illlegalPathRegex) {
+ it.value.toCharArray()
+ .joinToString("") { "__" + it.code.toString(16).padStart(4, '0') }
+ })
override fun toString(): String {
return neuItem
@@ -53,7 +62,7 @@ value class SkyblockId(val neuItem: String) : Comparable<SkyblockId> {
}
/**
- * A bazaar stock item id, as returned by the HyPixel bazaar api endpoint.
+ * A bazaar stock item id, as returned by the Hypixel bazaar api endpoint.
* These are not equivalent to the in-game ids, or the NEU repo ids, and in fact, do not refer to items, but instead
* to bazaar stocks. The main difference from [SkyblockId]s is concerning enchanted books. There are probably more,
* but for now this holds.
@@ -61,11 +70,10 @@ value class SkyblockId(val neuItem: String) : Comparable<SkyblockId> {
@JvmInline
@Serializable
value class BazaarStock(val bazaarId: String) {
- fun toRepoId(): SkyblockId {
- bazaarEnchantmentRegex.matchEntire(bazaarId)?.let {
- return SkyblockId("${it.groupValues[1]};${it.groupValues[2]}")
+ companion object {
+ fun fromSkyBlockId(skyblockId: SkyblockId): BazaarStock {
+ return BazaarStock(RepoManager.neuRepo.constants.bazaarStocks.getBazaarStockOrDefault(skyblockId.neuItem))
}
- return SkyblockId(bazaarId.replace(":", "-"))
}
}
@@ -84,7 +92,9 @@ value class SkyblockId(val neuItem: String) : Comparable<SkyblockId> {
val NEUItem.skyblockId get() = SkyblockId(skyblockItemId)
val NEUIngredient.skyblockId get() = SkyblockId(itemId)
+val SkyblockId.asBazaarStock get() = SkyblockId.BazaarStock.fromSkyBlockId(this)
+@ExpensiveItemCacheApi
fun NEUItem.guessRecipeId(): String? {
if (!skyblockItemId.contains(";")) return skyblockItemId
val item = this.asItemStack()
@@ -103,9 +113,11 @@ data class HypixelPetInfo(
val exp: Double = 0.0,
val candyUsed: Int = 0,
val uuid: UUID? = null,
- val active: Boolean = false,
+ val active: Boolean? = false,
+ val heldItem: String? = null,
) {
val skyblockId get() = SkyblockId("${type.uppercase()};${tier.ordinal}") // TODO: is this ordinal set up correctly?
+ val level get() = ExpLadders.getExpLadder(type, tier).getPetLevel(exp)
}
private val jsonparser = Json { ignoreUnknownKeys = true }
@@ -130,13 +142,38 @@ fun ItemStack.modifyExtraAttributes(block: (NbtCompound) -> Unit) {
}
val ItemStack.skyblockUUIDString: String?
- get() = extraAttributes.getString("uuid")?.takeIf { it.isNotBlank() }
+ get() = extraAttributes.getString("uuid").getOrNull()?.takeIf { it.isNotBlank() }
+
+private val timestampFormat = //"10/11/21 3:39 PM"
+ DateTimeFormatterBuilder().apply {
+ appendValue(ChronoField.MONTH_OF_YEAR, 2)
+ appendLiteral("/")
+ appendValue(ChronoField.DAY_OF_MONTH, 2)
+ appendLiteral("/")
+ appendValueReduced(ChronoField.YEAR, 2, 2, 1950)
+ appendLiteral(" ")
+ appendValue(ChronoField.HOUR_OF_AMPM, 1, 2, SignStyle.NEVER)
+ appendLiteral(":")
+ appendValue(ChronoField.MINUTE_OF_HOUR, 2)
+ appendLiteral(" ")
+ appendText(ChronoField.AMPM_OF_DAY)
+ }.toFormatter()
+val ItemStack.timestamp
+ get() =
+ extraAttributes.getLong("timestamp").getOrNull()?.let { Instant.ofEpochMilli(it) }
+ ?: extraAttributes.getString("timestamp").getOrNull()?.let {
+ ErrorUtil.catch("Could not parse timestamp $it") {
+ LocalDateTime.from(timestampFormat.parse(it)).atZone(SBData.hypixelTimeZone)
+ .toInstant()
+ }.orNull()
+ }
val ItemStack.skyblockUUID: UUID?
get() = skyblockUUIDString?.let { UUID.fromString(it) }
private val petDataCache = WeakCache.memoize<ItemStack, Optional<HypixelPetInfo>>("PetInfo") {
val jsonString = it.extraAttributes.getString("petInfo")
+ .getOrNull()
if (jsonString.isNullOrBlank()) return@memoize Optional.empty()
ErrorUtil.catch<HypixelPetInfo?>("Could not decode hypixel pet info") {
jsonparser.decodeFromString<HypixelPetInfo>(jsonString)
@@ -145,8 +182,8 @@ private val petDataCache = WeakCache.memoize<ItemStack, Optional<HypixelPetInfo>
}
fun ItemStack.getUpgradeStars(): Int {
- return extraAttributes.getInt("upgrade_level").takeIf { it > 0 }
- ?: extraAttributes.getInt("dungeon_item_level").takeIf { it > 0 }
+ return extraAttributes.getInt("upgrade_level").getOrNull()?.takeIf { it > 0 }
+ ?: extraAttributes.getInt("dungeon_item_level").getOrNull()?.takeIf { it > 0 }
?: 0
}
@@ -155,7 +192,7 @@ fun ItemStack.getUpgradeStars(): Int {
value class ReforgeId(val id: String)
fun ItemStack.getReforgeId(): ReforgeId? {
- return extraAttributes.getString("modifier").takeIf { it.isNotBlank() }?.let(::ReforgeId)
+ return extraAttributes.getString("modifier").getOrNull()?.takeIf { it.isNotBlank() }?.let(::ReforgeId)
}
val ItemStack.petData: HypixelPetInfo?
@@ -169,8 +206,8 @@ fun ItemStack.setSkyBlockId(skyblockId: SkyblockId): ItemStack {
val ItemStack.skyBlockId: SkyblockId?
get() {
- return when (val id = extraAttributes.getString("id")) {
- "" -> {
+ return when (val id = extraAttributes.getString("id").getOrNull()) {
+ "", null -> {
null
}
@@ -180,25 +217,67 @@ val ItemStack.skyBlockId: SkyblockId?
"RUNE", "UNIQUE_RUNE" -> {
val runeData = extraAttributes.getCompound("runes")
- val runeKind = runeData.keys.singleOrNull()
+ .getOrNull()
+ val runeKind = runeData?.keys?.singleOrNull()
if (runeKind == null) SkyblockId("RUNE")
- else SkyblockId("${runeKind.uppercase()}_RUNE;${runeData.getInt(runeKind)}")
+ else SkyblockId("${runeKind.uppercase()}_RUNE;${runeData.getInt(runeKind).getOrNull()}")
}
"ABICASE" -> {
- SkyblockId("ABICASE_${extraAttributes.getString("model").uppercase()}")
+ SkyblockId("ABICASE_${extraAttributes.getString("model").getOrNull()?.uppercase()}")
}
"ENCHANTED_BOOK" -> {
val enchantmentData = extraAttributes.getCompound("enchantments")
- val enchantName = enchantmentData.keys.singleOrNull()
+ .getOrNull()
+ val enchantName = enchantmentData?.keys?.singleOrNull()
if (enchantName == null) SkyblockId("ENCHANTED_BOOK")
- else SkyblockId("${enchantName.uppercase()};${enchantmentData.getInt(enchantName)}")
+ else SkyblockId("${enchantName.uppercase()};${enchantmentData.getInt(enchantName).getOrNull()}")
+ }
+
+ "ATTRIBUTE_SHARD" -> {
+ val attributeData = extraAttributes.getCompound("attributes").getOrNull()
+ val attributeName = attributeData?.keys?.singleOrNull()
+ if (attributeName == null) SkyblockId("ATTRIBUTE_SHARD")
+ else SkyblockId(
+ "ATTRIBUTE_SHARD_${attributeName.uppercase()};${
+ attributeData.getInt(attributeName).getOrNull()
+ }"
+ )
+ }
+
+ "POTION" -> {
+ val potionData = extraAttributes.getString("potion").getOrNull()
+ val potionName = extraAttributes.getString("potion_name").getOrNull()
+ val potionLevel = extraAttributes.getInt("potion_level").getOrNull()
+ val potionType = extraAttributes.getString("potion_type").getOrNull()
+ when {
+ potionName != null -> SkyblockId("POTION_${potionName.uppercase()};$potionLevel")
+ potionData != null -> SkyblockId("POTION_${potionData.uppercase()};$potionLevel")
+ potionType != null -> SkyblockId("POTION_${potionType.uppercase()}")
+ else -> SkyblockId("WATER_BOTTLE")
+ }
+ }
+
+ "PARTY_HAT_SLOTH", "PARTY_HAT_CRAB", "PARTY_HAT_CRAB_ANIMATED" -> {
+ val partyHatEmoji = extraAttributes.getString("party_hat_emoji").getOrNull()
+ val partyHatYear = extraAttributes.getInt("party_hat_year").getOrNull()
+ val partyHatColor = extraAttributes.getString("party_hat_color").getOrNull()
+ when {
+ partyHatEmoji != null -> SkyblockId("PARTY_HAT_SLOTH_${partyHatEmoji.uppercase()}")
+ partyHatYear == 2022 -> SkyblockId("PARTY_HAT_CRAB_${partyHatColor?.uppercase()}_ANIMATED")
+ else -> SkyblockId("PARTY_HAT_CRAB_${partyHatColor?.uppercase()}")
+ }
+ }
+
+ "BALLOON_HAT_2024", "BALLOON_HAT_2025" -> {
+ val partyHatYear = extraAttributes.getInt("party_hat_year").getOrNull()
+ val partyHatColor = extraAttributes.getString("party_hat_color").getOrNull()
+ SkyblockId("BALLOON_HAT_${partyHatYear}_${partyHatColor?.uppercase()}")
}
- // TODO: PARTY_HAT_CRAB{,_ANIMATED,_SLOTH},POTION
else -> {
- SkyblockId(id)
+ SkyblockId(id.replace(":", "-"))
}
}
}
diff --git a/src/main/kotlin/util/StringUtil.kt b/src/main/kotlin/util/StringUtil.kt
index 68e161a..dc98dc0 100644
--- a/src/main/kotlin/util/StringUtil.kt
+++ b/src/main/kotlin/util/StringUtil.kt
@@ -9,6 +9,8 @@ object StringUtil {
return string.replace(",", "").toInt()
}
+ fun String.title() = replaceFirstChar { it.titlecase() }
+
fun Iterable<String>.unwords() = joinToString(" ")
fun nextLexicographicStringOfSameLength(string: String): String {
val next = StringBuilder(string)
diff --git a/src/main/kotlin/util/TestUtil.kt b/src/main/kotlin/util/TestUtil.kt
index 45e3dde..da8ba38 100644
--- a/src/main/kotlin/util/TestUtil.kt
+++ b/src/main/kotlin/util/TestUtil.kt
@@ -2,6 +2,7 @@ package moe.nea.firmament.util
object TestUtil {
inline fun <T> unlessTesting(block: () -> T): T? = if (isInTest) null else block()
+ @JvmField
val isInTest =
Thread.currentThread().stackTrace.any {
it.className.startsWith("org.junit.") || it.className.startsWith("io.kotest.")
diff --git a/src/main/kotlin/util/asm/AsmAnnotationUtil.kt b/src/main/kotlin/util/asm/AsmAnnotationUtil.kt
new file mode 100644
index 0000000..fb0e92c
--- /dev/null
+++ b/src/main/kotlin/util/asm/AsmAnnotationUtil.kt
@@ -0,0 +1,89 @@
+package moe.nea.firmament.util.asm
+
+import com.google.common.base.Defaults
+import java.lang.reflect.InvocationHandler
+import java.lang.reflect.Method
+import java.lang.reflect.Proxy
+import org.objectweb.asm.Type
+import org.objectweb.asm.tree.AnnotationNode
+
+object AsmAnnotationUtil {
+ class AnnotationProxy(
+ val originalType: Class<out Annotation>,
+ val annotationNode: AnnotationNode,
+ ) : InvocationHandler {
+ val offsets = annotationNode.values.withIndex()
+ .chunked(2)
+ .map { it.first() }
+ .associate { (idx, value) -> value as String to idx + 1 }
+
+ fun nestArrayType(depth: Int, comp: Class<*>): Class<*> =
+ if (depth == 0) comp
+ else java.lang.reflect.Array.newInstance(nestArrayType(depth - 1, comp), 0).javaClass
+
+ fun unmap(
+ value: Any?,
+ comp: Class<*>,
+ depth: Int,
+ ): Any? {
+ value ?: return null
+ if (depth > 0)
+ return ((value as List<Any>)
+ .map { unmap(it, comp, depth - 1) } as java.util.List<Any>)
+ .toArray(java.lang.reflect.Array.newInstance(nestArrayType(depth - 1, comp), 0) as Array<*>)
+ if (comp.isEnum) {
+ comp as Class<out Enum<*>>
+ when (value) {
+ is String -> return java.lang.Enum.valueOf(comp, value)
+ is List<*> -> return java.lang.Enum.valueOf(comp, value[1] as String)
+ else -> error("Unknown enum variant $value for $comp")
+ }
+ }
+ when (value) {
+ is Type -> return Class.forName(value.className)
+ is AnnotationNode -> return createProxy(comp as Class<out Annotation>, value)
+ is String, is Boolean, is Byte, is Double, is Int, is Float, is Long, is Short, is Char -> return value
+ }
+ error("Unknown enum variant $value for $comp")
+ }
+
+ fun defaultFor(fullType: Class<*>): Any? {
+ if (fullType.isArray) return java.lang.reflect.Array.newInstance(fullType.componentType, 0)
+ if (fullType.isPrimitive) {
+ return Defaults.defaultValue(fullType)
+ }
+ if (fullType == String::class.java)
+ return ""
+ return null
+ }
+
+ override fun invoke(
+ proxy: Any,
+ method: Method,
+ args: Array<out Any?>?
+ ): Any? {
+ val name = method.name
+ val ret = method.returnType
+ val retU = generateSequence(ret) { if (it.isArray) it.componentType else null }
+ .toList()
+ val arrayDepth = retU.size - 1
+ val componentType = retU.last()
+
+ val off = offsets[name]
+ if (off == null) {
+ return defaultFor(ret)
+ }
+ return unmap(annotationNode.values[off], componentType, arrayDepth)
+ }
+ }
+
+ fun <T : Annotation> createProxy(
+ annotationClass: Class<T>,
+ annotationNode: AnnotationNode
+ ): T {
+ require(Type.getType(annotationClass) == Type.getType(annotationNode.desc))
+ return Proxy.newProxyInstance(javaClass.classLoader,
+ arrayOf(annotationClass),
+ AnnotationProxy(annotationClass, annotationNode)) as T
+ }
+}
diff --git a/src/main/kotlin/util/async/input.kt b/src/main/kotlin/util/async/input.kt
index f22c595..2c546ba 100644
--- a/src/main/kotlin/util/async/input.kt
+++ b/src/main/kotlin/util/async/input.kt
@@ -1,47 +1,89 @@
-
-
package moe.nea.firmament.util.async
+import io.github.notenoughupdates.moulconfig.gui.GuiContext
+import io.github.notenoughupdates.moulconfig.gui.component.CenterComponent
+import io.github.notenoughupdates.moulconfig.gui.component.ColumnComponent
+import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent
+import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
+import io.github.notenoughupdates.moulconfig.gui.component.TextFieldComponent
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
+import net.minecraft.client.gui.screen.Screen
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
+import moe.nea.firmament.gui.FirmButtonComponent
import moe.nea.firmament.keybindings.IKeyBinding
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.MoulConfigUtils
+import moe.nea.firmament.util.ScreenUtil
private object InputHandler {
- data class KeyInputContinuation(val keybind: IKeyBinding, val onContinue: () -> Unit)
-
- private val activeContinuations = mutableListOf<KeyInputContinuation>()
-
- fun registerContinuation(keyInputContinuation: KeyInputContinuation): () -> Unit {
- synchronized(InputHandler) {
- activeContinuations.add(keyInputContinuation)
- }
- return {
- synchronized(this) {
- activeContinuations.remove(keyInputContinuation)
- }
- }
- }
-
- init {
- HandledScreenKeyPressedEvent.subscribe("Input:resumeAfterInput") { event ->
- synchronized(InputHandler) {
- val toRemove = activeContinuations.filter {
- event.matches(it.keybind)
- }
- toRemove.forEach { it.onContinue() }
- activeContinuations.removeAll(toRemove)
- }
- }
- }
+ data class KeyInputContinuation(val keybind: IKeyBinding, val onContinue: () -> Unit)
+
+ private val activeContinuations = mutableListOf<KeyInputContinuation>()
+
+ fun registerContinuation(keyInputContinuation: KeyInputContinuation): () -> Unit {
+ synchronized(InputHandler) {
+ activeContinuations.add(keyInputContinuation)
+ }
+ return {
+ synchronized(this) {
+ activeContinuations.remove(keyInputContinuation)
+ }
+ }
+ }
+
+ init {
+ HandledScreenKeyPressedEvent.subscribe("Input:resumeAfterInput") { event ->
+ synchronized(InputHandler) {
+ val toRemove = activeContinuations.filter {
+ event.matches(it.keybind)
+ }
+ toRemove.forEach { it.onContinue() }
+ activeContinuations.removeAll(toRemove)
+ }
+ }
+ }
}
suspend fun waitForInput(keybind: IKeyBinding): Unit = suspendCancellableCoroutine { cont ->
- val unregister =
- InputHandler.registerContinuation(InputHandler.KeyInputContinuation(keybind) { cont.resume(Unit) })
- cont.invokeOnCancellation {
- unregister()
- }
+ val unregister =
+ InputHandler.registerContinuation(InputHandler.KeyInputContinuation(keybind) { cont.resume(Unit) })
+ cont.invokeOnCancellation {
+ unregister()
+ }
}
+fun createPromptScreenGuiComponent(suggestion: String, prompt: String, action: Runnable) = (run {
+ val text = GetSetter.floating(suggestion)
+ GuiContext(
+ CenterComponent(
+ PanelComponent(
+ ColumnComponent(
+ TextFieldComponent(text, 120),
+ FirmButtonComponent(TextComponent(prompt), action = action)
+ )
+ )
+ )
+ ) to text
+})
+
+suspend fun waitForTextInput(suggestion: String, prompt: String) =
+ suspendCancellableCoroutine<String> { cont ->
+ lateinit var screen: Screen
+ lateinit var text: GetSetter<String>
+ val action = {
+ if (MC.screen === screen)
+ MC.screen = null
+ // TODO: should this exit
+ cont.resume(text.get())
+ }
+ val (gui, text_) = createPromptScreenGuiComponent(suggestion, prompt, action)
+ text = text_
+ screen = MoulConfigUtils.wrapScreen(gui, null, onClose = action)
+ ScreenUtil.setScreenLater(screen)
+ cont.invokeOnCancellation {
+ action()
+ }
+ }
diff --git a/src/main/kotlin/util/collections/RangeUtil.kt b/src/main/kotlin/util/collections/RangeUtil.kt
new file mode 100644
index 0000000..a7029ac
--- /dev/null
+++ b/src/main/kotlin/util/collections/RangeUtil.kt
@@ -0,0 +1,40 @@
+package moe.nea.firmament.util.collections
+
+import kotlin.math.floor
+
+val ClosedFloatingPointRange<Float>.centre get() = (endInclusive + start) / 2
+
+fun ClosedFloatingPointRange<Float>.nonNegligibleSubSectionsAlignedWith(
+ interval: Float
+): Iterable<Float> {
+ require(interval.isFinite())
+ val range = this
+ return object : Iterable<Float> {
+ override fun iterator(): Iterator<Float> {
+ return object : FloatIterator() {
+ var polledValue: Float = range.start
+ var lastValue: Float = polledValue
+
+ override fun nextFloat(): Float {
+ if (!hasNext()) throw NoSuchElementException()
+ lastValue = polledValue
+ polledValue = Float.NaN
+ return lastValue
+ }
+
+ override fun hasNext(): Boolean {
+ if (!polledValue.isNaN()) {
+ return true
+ }
+ if (lastValue == range.endInclusive)
+ return false
+ polledValue = (floor(lastValue / interval) + 1) * interval
+ if (polledValue > range.endInclusive) {
+ polledValue = range.endInclusive
+ }
+ return true
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/util/collections/WeakCache.kt b/src/main/kotlin/util/collections/WeakCache.kt
index 38f9886..4a48c63 100644
--- a/src/main/kotlin/util/collections/WeakCache.kt
+++ b/src/main/kotlin/util/collections/WeakCache.kt
@@ -9,102 +9,108 @@ import moe.nea.firmament.features.debug.DebugLogger
* the key. Each key can have additional extra data that is used to look up values. That extra data is not required to
* be a life reference. The main Key is compared using strict reference equality. This map is not synchronized.
*/
-class WeakCache<Key : Any, ExtraKey : Any, Value : Any>(val name: String) {
- private val queue = object : ReferenceQueue<Key>() {}
- private val map = mutableMapOf<Ref, Value>()
-
- val size: Int
- get() {
- clearOldReferences()
- return map.size
- }
-
- fun clearOldReferences() {
- var successCount = 0
- var totalCount = 0
- while (true) {
- val reference = queue.poll() ?: break
- totalCount++
- if (map.remove(reference) != null)
- successCount++
- }
- if (totalCount > 0)
- logger.log { "Cleared $successCount/$totalCount references from queue" }
- }
-
- fun get(key: Key, extraData: ExtraKey): Value? {
- clearOldReferences()
- return map[Ref(key, extraData)]
- }
-
- fun put(key: Key, extraData: ExtraKey, value: Value) {
- clearOldReferences()
- map[Ref(key, extraData)] = value
- }
-
- fun getOrPut(key: Key, extraData: ExtraKey, value: (Key, ExtraKey) -> Value): Value {
- clearOldReferences()
- return map.getOrPut(Ref(key, extraData)) { value(key, extraData) }
- }
-
- fun clear() {
- map.clear()
- }
-
- init {
- allInstances.add(this)
- }
-
- companion object {
- val allInstances = InstanceList<WeakCache<*, *, *>>("WeakCaches")
- private val logger = DebugLogger("WeakCache")
- fun <Key : Any, Value : Any> memoize(name: String, function: (Key) -> Value):
- CacheFunction.NoExtraData<Key, Value> {
- return CacheFunction.NoExtraData(WeakCache(name), function)
- }
-
- fun <Key : Any, ExtraKey : Any, Value : Any> memoize(name: String, function: (Key, ExtraKey) -> Value):
- CacheFunction.WithExtraData<Key, ExtraKey, Value> {
- return CacheFunction.WithExtraData(WeakCache(name), function)
- }
- }
-
- inner class Ref(
- weakInstance: Key,
- val extraData: ExtraKey,
- ) : WeakReference<Key>(weakInstance, queue) {
- val hashCode = System.identityHashCode(weakInstance) * 31 + extraData.hashCode()
- override fun equals(other: Any?): Boolean {
- if (other !is WeakCache<*, *, *>.Ref) return false
- return other.hashCode == this.hashCode
- && other.get() === this.get()
- && other.extraData == this.extraData
- }
-
- override fun hashCode(): Int {
- return hashCode
- }
- }
-
- interface CacheFunction {
- val cache: WeakCache<*, *, *>
-
- data class NoExtraData<Key : Any, Value : Any>(
- override val cache: WeakCache<Key, Unit, Value>,
- val wrapped: (Key) -> Value,
- ) : CacheFunction, (Key) -> Value {
- override fun invoke(p1: Key): Value {
- return cache.getOrPut(p1, Unit, { a, _ -> wrapped(a) })
- }
- }
-
- data class WithExtraData<Key : Any, ExtraKey : Any, Value : Any>(
- override val cache: WeakCache<Key, ExtraKey, Value>,
- val wrapped: (Key, ExtraKey) -> Value,
- ) : CacheFunction, (Key, ExtraKey) -> Value {
- override fun invoke(p1: Key, p2: ExtraKey): Value {
- return cache.getOrPut(p1, p2, wrapped)
- }
- }
- }
+open class WeakCache<Key : Any, ExtraKey : Any, Value : Any>(val name: String) {
+ private val queue = object : ReferenceQueue<Key>() {}
+ private val map = mutableMapOf<Ref, Value>()
+
+ val size: Int
+ get() {
+ clearOldReferences()
+ return map.size
+ }
+
+ fun clearOldReferences() {
+ var successCount = 0
+ var totalCount = 0
+ while (true) {
+ val reference = queue.poll() as WeakCache<*, *, *>.Ref? ?: break
+ totalCount++
+ if (reference.shouldBeEvicted() && map.remove(reference) != null)
+ successCount++
+ }
+ if (totalCount > 0)
+ logger.log("Cleared $successCount/$totalCount references from queue")
+ }
+
+ open fun mkRef(key: Key, extraData: ExtraKey): Ref {
+ return Ref(key, extraData)
+ }
+
+ fun get(key: Key, extraData: ExtraKey): Value? {
+ clearOldReferences()
+ return map[mkRef(key, extraData)]
+ }
+
+ fun put(key: Key, extraData: ExtraKey, value: Value) {
+ clearOldReferences()
+ map[mkRef(key, extraData)] = value
+ }
+
+ fun getOrPut(key: Key, extraData: ExtraKey, value: (Key, ExtraKey) -> Value): Value {
+ clearOldReferences()
+ return map.getOrPut(mkRef(key, extraData)) { value(key, extraData) }
+ }
+
+ fun clear() {
+ map.clear()
+ }
+
+ init {
+ allInstances.add(this)
+ }
+
+ companion object {
+ val allInstances = InstanceList<WeakCache<*, *, *>>("WeakCaches")
+ private val logger = DebugLogger("WeakCache")
+ fun <Key : Any, Value : Any> memoize(name: String, function: (Key) -> Value):
+ CacheFunction.NoExtraData<Key, Value> {
+ return CacheFunction.NoExtraData(WeakCache(name), function)
+ }
+
+ fun <Key : Any, ExtraKey : Any, Value : Any> dontMemoize(name: String, function: (Key, ExtraKey) -> Value) = function
+ fun <Key : Any, ExtraKey : Any, Value : Any> memoize(name: String, function: (Key, ExtraKey) -> Value):
+ CacheFunction.WithExtraData<Key, ExtraKey, Value> {
+ return CacheFunction.WithExtraData(WeakCache(name), function)
+ }
+ }
+
+ open inner class Ref(
+ weakInstance: Key,
+ val extraData: ExtraKey,
+ ) : WeakReference<Key>(weakInstance, queue) {
+ open fun shouldBeEvicted() = true
+ val hashCode = System.identityHashCode(weakInstance) * 31 + extraData.hashCode()
+ override fun equals(other: Any?): Boolean {
+ if (other !is WeakCache<*, *, *>.Ref) return false
+ return other.hashCode == this.hashCode
+ && other.get() === this.get()
+ && other.extraData == this.extraData
+ }
+
+ override fun hashCode(): Int {
+ return hashCode
+ }
+ }
+
+ interface CacheFunction {
+ val cache: WeakCache<*, *, *>
+
+ data class NoExtraData<Key : Any, Value : Any>(
+ override val cache: WeakCache<Key, Unit, Value>,
+ val wrapped: (Key) -> Value,
+ ) : CacheFunction, (Key) -> Value {
+ override fun invoke(p1: Key): Value {
+ return cache.getOrPut(p1, Unit, { a, _ -> wrapped(a) })
+ }
+ }
+
+ data class WithExtraData<Key : Any, ExtraKey : Any, Value : Any>(
+ override val cache: WeakCache<Key, ExtraKey, Value>,
+ val wrapped: (Key, ExtraKey) -> Value,
+ ) : CacheFunction, (Key, ExtraKey) -> Value {
+ override fun invoke(p1: Key, p2: ExtraKey): Value {
+ return cache.getOrPut(p1, p2, wrapped)
+ }
+ }
+ }
}
diff --git a/src/main/kotlin/util/compatloader/CompatLoader.kt b/src/main/kotlin/util/compatloader/CompatLoader.kt
index 6b60e87..d1073af 100644
--- a/src/main/kotlin/util/compatloader/CompatLoader.kt
+++ b/src/main/kotlin/util/compatloader/CompatLoader.kt
@@ -6,7 +6,7 @@ import kotlin.reflect.KClass
import kotlin.streams.asSequence
import moe.nea.firmament.Firmament
-abstract class CompatLoader<T : Any>(val kClass: Class<T>) {
+open class CompatLoader<T : Any>(val kClass: Class<T>) {
constructor(kClass: KClass<T>) : this(kClass.java)
val loader: ServiceLoader<T> = ServiceLoader.load(kClass)
diff --git a/src/main/kotlin/util/compatloader/CompatMeta.kt b/src/main/kotlin/util/compatloader/CompatMeta.kt
new file mode 100644
index 0000000..cf63645
--- /dev/null
+++ b/src/main/kotlin/util/compatloader/CompatMeta.kt
@@ -0,0 +1,48 @@
+package moe.nea.firmament.util.compatloader
+
+import java.util.ServiceLoader
+import moe.nea.firmament.events.subscription.SubscriptionList
+import moe.nea.firmament.init.AutoDiscoveryPlugin
+import moe.nea.firmament.util.ErrorUtil
+
+/**
+ * Declares the compat meta interface for the current source set.
+ * This is used by [CompatLoader], [SubscriptionList], and [AutoDiscoveryPlugin]. Annotate a [ICompatMeta] object with
+ * this.
+ */
+annotation class CompatMeta
+
+interface ICompatMetaGen {
+ fun owns(className: String): Boolean
+ val meta: ICompatMeta
+}
+
+interface ICompatMeta {
+ fun shouldLoad(): Boolean
+
+ companion object {
+ val allMetas = ServiceLoader
+ .load(ICompatMetaGen::class.java)
+ .toList()
+
+ fun shouldLoad(className: String): Boolean {
+ // TODO: replace this with a more performant package lookup
+ val meta = if (ErrorUtil.aggressiveErrors) {
+ val fittingMetas = allMetas.filter { it.owns(className) }
+ require(fittingMetas.size == 1) { "Orphaned or duplicate owned class $className (${fittingMetas.map { it.meta }}). Consider adding a @CompatMeta object." }
+ fittingMetas.single()
+ } else {
+ allMetas.firstOrNull { it.owns(className) }
+ }
+ return meta?.meta?.shouldLoad() ?: true
+ }
+ }
+}
+
+object CompatHelper {
+ fun isOwnedByPackage(className: String, vararg packages: String): Boolean {
+ // TODO: create package lookup structure once
+ val packageName = className.substringBeforeLast('.')
+ return packageName in packages
+ }
+}
diff --git a/src/main/kotlin/util/data/MultiFileDataHolder.kt b/src/main/kotlin/util/data/MultiFileDataHolder.kt
new file mode 100644
index 0000000..94c6f05
--- /dev/null
+++ b/src/main/kotlin/util/data/MultiFileDataHolder.kt
@@ -0,0 +1,63 @@
+package moe.nea.firmament.util.data
+
+import kotlinx.serialization.KSerializer
+import kotlin.io.path.createDirectories
+import kotlin.io.path.deleteExisting
+import kotlin.io.path.exists
+import kotlin.io.path.extension
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.nameWithoutExtension
+import kotlin.io.path.readText
+import kotlin.io.path.writeText
+import moe.nea.firmament.Firmament
+
+abstract class MultiFileDataHolder<T>(
+ val dataSerializer: KSerializer<T>,
+ val configName: String
+) { // TODO: abstract this + ProfileSpecificDataHolder
+ val configDirectory = Firmament.CONFIG_DIR.resolve(configName)
+ private var allData = readValues()
+ protected fun readValues(): MutableMap<String, T> {
+ if (!configDirectory.exists()) {
+ configDirectory.createDirectories()
+ }
+ val profileFiles = configDirectory.listDirectoryEntries()
+ return profileFiles
+ .filter { it.extension == "json" }
+ .mapNotNull {
+ try {
+ it.nameWithoutExtension to Firmament.json.decodeFromString(dataSerializer, it.readText())
+ } catch (e: Exception) { /* Expecting IOException and SerializationException, but Kotlin doesn't allow multi catches*/
+ IDataHolder.badLoads.add(configName)
+ Firmament.logger.error(
+ "Exception during loading of multi file data holder $it ($configName). This will reset that profiles config.",
+ e
+ )
+ null
+ }
+ }.toMap().toMutableMap()
+ }
+
+ fun save() {
+ if (!configDirectory.exists()) {
+ configDirectory.createDirectories()
+ }
+ val c = allData
+ configDirectory.listDirectoryEntries().forEach {
+ if (it.nameWithoutExtension !in c.mapKeys { it.toString() }) {
+ it.deleteExisting()
+ }
+ }
+ c.forEach { (name, value) ->
+ val f = configDirectory.resolve("$name.json")
+ f.writeText(Firmament.json.encodeToString(dataSerializer, value))
+ }
+ }
+
+ fun list(): Map<String, T> = allData
+ val validPathRegex = "[a-zA-Z0-9_][a-zA-Z0-9\\-_.]*".toPattern()
+ fun insert(name: String, value: T) {
+ require(validPathRegex.matcher(name).matches()) { "Not a valid name: $name" }
+ allData[name] = value
+ }
+}
diff --git a/src/main/kotlin/util/json/DashlessUUIDSerializer.kt b/src/main/kotlin/util/json/DashlessUUIDSerializer.kt
index acb1dc8..6bafebe 100644
--- a/src/main/kotlin/util/json/DashlessUUIDSerializer.kt
+++ b/src/main/kotlin/util/json/DashlessUUIDSerializer.kt
@@ -10,6 +10,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import moe.nea.firmament.util.parseDashlessUUID
+import moe.nea.firmament.util.parsePotentiallyDashlessUUID
object DashlessUUIDSerializer : KSerializer<UUID> {
override val descriptor: SerialDescriptor =
@@ -17,10 +18,7 @@ object DashlessUUIDSerializer : KSerializer<UUID> {
override fun deserialize(decoder: Decoder): UUID {
val str = decoder.decodeString()
- if ("-" in str) {
- return UUID.fromString(str)
- }
- return parseDashlessUUID(str)
+ return parsePotentiallyDashlessUUID(str)
}
override fun serialize(encoder: Encoder, value: UUID) {
diff --git a/src/main/kotlin/util/json/KJsonUtils.kt b/src/main/kotlin/util/json/KJsonUtils.kt
new file mode 100644
index 0000000..b15119b
--- /dev/null
+++ b/src/main/kotlin/util/json/KJsonUtils.kt
@@ -0,0 +1,11 @@
+package moe.nea.firmament.util.json
+
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonPrimitive
+
+fun <T : JsonElement> List<T>.asJsonArray(): JsonArray {
+ return JsonArray(this)
+}
+
+fun Iterable<String>.toJsonArray(): JsonArray = map { JsonPrimitive(it) }.asJsonArray()
diff --git a/src/main/kotlin/util/math/GChainReconciliation.kt b/src/main/kotlin/util/math/GChainReconciliation.kt
new file mode 100644
index 0000000..37998d5
--- /dev/null
+++ b/src/main/kotlin/util/math/GChainReconciliation.kt
@@ -0,0 +1,102 @@
+package moe.nea.firmament.util.math
+
+import kotlin.math.min
+
+/**
+ * Algorithm for (sort of) cheap reconciliation of two cycles with missing frames.
+ */
+object GChainReconciliation {
+ // Step one: Find the most common element and shift the arrays until it is at the start in both (this could be just rotating until minimal levenshtein distance or smth. that would be way better for cycles with duplicates, but i do not want to implement levenshtein as well)
+ // Step two: Find the first different element.
+ // Step three: Find the next index of both of the elements.
+ // Step four: Insert the element that is further away.
+
+ fun <T> Iterable<T>.frequencies(): Map<T, Int> {
+ val acc = mutableMapOf<T, Int>()
+ for (t in this) {
+ acc.compute(t, { _, old -> (old ?: 0) + 1 })
+ }
+ return acc
+ }
+
+ fun <T> findMostCommonlySharedElement(
+ leftChain: List<T>,
+ rightChain: List<T>,
+ ): T {
+ val lf = leftChain.frequencies()
+ val rf = rightChain.frequencies()
+ val mostCommonlySharedElement = lf.maxByOrNull { min(it.value, rf[it.key] ?: 0) }?.key
+ if (mostCommonlySharedElement == null || mostCommonlySharedElement !in rf)
+ error("Could not find a shared element")
+ return mostCommonlySharedElement
+ }
+
+ fun <T> List<T>.getMod(index: Int): T {
+ return this[index.mod(size)]
+ }
+
+ fun <T> List<T>.rotated(offset: Int): List<T> {
+ val newList = mutableListOf<T>()
+ for (index in indices) {
+ newList.add(getMod(index - offset))
+ }
+ return newList
+ }
+
+ fun <T> shiftToFront(list: List<T>, element: T): List<T> {
+ val shiftDistance = list.indexOf(element)
+ require(shiftDistance >= 0)
+ return list.rotated(-shiftDistance)
+ }
+
+ fun <T> List<T>.indexOfOrMaxInt(element: T): Int = indexOf(element).takeUnless { it < 0 } ?: Int.MAX_VALUE
+
+ fun <T> reconcileCycles(
+ leftChain: List<T>,
+ rightChain: List<T>,
+ ): List<T> {
+ val mostCommonElement = findMostCommonlySharedElement(leftChain, rightChain)
+ val left = shiftToFront(leftChain, mostCommonElement).toMutableList()
+ val right = shiftToFront(rightChain, mostCommonElement).toMutableList()
+
+ var index = 0
+ while (index < left.size && index < right.size) {
+ val leftEl = left[index]
+ val rightEl = right[index]
+ if (leftEl == rightEl) {
+ index++
+ continue
+ }
+ val nextLeftInRight = right.subList(index, right.size)
+ .indexOfOrMaxInt(leftEl)
+
+ val nextRightInLeft = left.subList(index, left.size)
+ .indexOfOrMaxInt(rightEl)
+ if (nextLeftInRight < nextRightInLeft) {
+ left.add(index, rightEl)
+ } else if (nextRightInLeft < nextLeftInRight) {
+ right.add(index, leftEl)
+ } else {
+ index++
+ }
+ }
+ return if (left.size < right.size) right else left
+ }
+
+ fun <T> isValidCycle(longList: List<T>, cycle: List<T>): Boolean {
+ for ((i, value) in longList.withIndex()) {
+ if (cycle.getMod(i) != value)
+ return false
+ }
+ return true
+ }
+
+ fun <T> List<T>.shortenCycle(): List<T> {
+ for (i in (1..<size)) {
+ if (isValidCycle(this, subList(0, i)))
+ return subList(0, i)
+ }
+ return this
+ }
+
+}
diff --git a/src/main/kotlin/util/math/Projections.kt b/src/main/kotlin/util/math/Projections.kt
new file mode 100644
index 0000000..359b21b
--- /dev/null
+++ b/src/main/kotlin/util/math/Projections.kt
@@ -0,0 +1,46 @@
+package moe.nea.firmament.util.math
+
+import kotlin.math.absoluteValue
+import kotlin.math.cos
+import kotlin.math.sin
+import net.minecraft.util.math.Vec2f
+import moe.nea.firmament.util.render.wrapAngle
+
+object Projections {
+ object Two {
+ val ε = 1e-6
+ val π = moe.nea.firmament.util.render.π
+ val τ = 2 * π
+
+ fun isNullish(float: Float) = float.absoluteValue < ε
+
+ fun xInterceptOfLine(origin: Vec2f, direction: Vec2f): Vec2f? {
+ if (isNullish(direction.x))
+ return Vec2f(origin.x, 0F)
+ if (isNullish(direction.y))
+ return null
+
+ val slope = direction.y / direction.x
+ return Vec2f(origin.x - origin.y / slope, 0F)
+ }
+
+ fun interceptAlongCardinal(distanceFromAxis: Float, slope: Float): Float? {
+ if (isNullish(slope))
+ return null
+ return -distanceFromAxis / slope
+ }
+
+ fun projectAngleOntoUnitBox(angleRadians: Double): Vec2f {
+ val angleRadians = wrapAngle(angleRadians)
+ val cx = cos(angleRadians)
+ val cy = sin(angleRadians)
+
+ val ex = 1 / cx.absoluteValue
+ val ey = 1 / cy.absoluteValue
+
+ val e = minOf(ex, ey)
+
+ return Vec2f((cx * e).toFloat(), (cy * e).toFloat())
+ }
+ }
+}
diff --git a/src/main/kotlin/util/mc/ArmorUtil.kt b/src/main/kotlin/util/mc/ArmorUtil.kt
new file mode 100644
index 0000000..fd1867c
--- /dev/null
+++ b/src/main/kotlin/util/mc/ArmorUtil.kt
@@ -0,0 +1,8 @@
+package moe.nea.firmament.util.mc
+
+import net.minecraft.entity.EquipmentSlot
+import net.minecraft.entity.LivingEntity
+
+val LivingEntity.iterableArmorItems
+ get() = EquipmentSlot.entries.asSequence()
+ .map { it to getEquippedStack(it) }
diff --git a/src/main/kotlin/util/mc/InitLevel.kt b/src/main/kotlin/util/mc/InitLevel.kt
new file mode 100644
index 0000000..2c3eedb
--- /dev/null
+++ b/src/main/kotlin/util/mc/InitLevel.kt
@@ -0,0 +1,25 @@
+package moe.nea.firmament.util.mc
+
+enum class InitLevel {
+ STARTING,
+ MC_INIT,
+ RENDER_INIT,
+ RENDER,
+ MAIN_MENU,
+ ;
+
+ companion object {
+ var initLevel = InitLevel.STARTING
+ private set
+
+ @JvmStatic
+ fun isAtLeast(wantedLevel: InitLevel): Boolean = initLevel >= wantedLevel
+
+ @JvmStatic
+ fun bump(nextLevel: InitLevel) {
+ if (nextLevel.ordinal != initLevel.ordinal + 1)
+ error("Cannot bump initLevel $nextLevel from $initLevel")
+ initLevel = nextLevel
+ }
+ }
+}
diff --git a/src/main/kotlin/util/mc/MCTabListAPI.kt b/src/main/kotlin/util/mc/MCTabListAPI.kt
new file mode 100644
index 0000000..66bdd55
--- /dev/null
+++ b/src/main/kotlin/util/mc/MCTabListAPI.kt
@@ -0,0 +1,96 @@
+package moe.nea.firmament.util.mc
+
+import com.mojang.serialization.Codec
+import com.mojang.serialization.codecs.RecordCodecBuilder
+import java.util.Optional
+import org.jetbrains.annotations.TestOnly
+import net.minecraft.client.gui.hud.PlayerListHud
+import net.minecraft.nbt.NbtOps
+import net.minecraft.scoreboard.Team
+import net.minecraft.text.Text
+import net.minecraft.text.TextCodecs
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.TickEvent
+import moe.nea.firmament.features.debug.DeveloperFeatures
+import moe.nea.firmament.features.debug.ExportedTestConstantMeta
+import moe.nea.firmament.mixins.accessor.AccessorPlayerListHud
+import moe.nea.firmament.util.ClipboardUtils
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.intoOptional
+import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString
+
+object MCTabListAPI {
+
+ fun PlayerListHud.cast() = this as AccessorPlayerListHud
+
+ @Subscribe
+ fun onTick(event: TickEvent) {
+ _currentTabList = null
+ }
+
+ @Subscribe
+ fun devCommand(event: CommandEvent.SubCommand) {
+ event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
+ thenLiteral("copytablist") {
+ thenExecute {
+ currentTabList.body.forEach {
+ MC.sendChat(Text.literal(TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, it).orThrow.toString()))
+ }
+ var compound = CurrentTabList.CODEC.encodeStart(NbtOps.INSTANCE, currentTabList).orThrow
+ compound = ExportedTestConstantMeta.SOURCE_CODEC.encode(
+ ExportedTestConstantMeta.current,
+ NbtOps.INSTANCE,
+ compound
+ ).orThrow
+ ClipboardUtils.setTextContent(
+ compound.toPrettyString()
+ )
+ }
+ }
+ }
+ }
+
+ @get:TestOnly
+ @set:TestOnly
+ var _currentTabList: CurrentTabList? = null
+
+ val currentTabList get() = _currentTabList ?: getTabListNow().also { _currentTabList = it }
+
+ data class CurrentTabList(
+ val header: Optional<Text>,
+ val footer: Optional<Text>,
+ val body: List<Text>,
+ ) {
+ companion object {
+ val CODEC: Codec<CurrentTabList> = RecordCodecBuilder.create {
+ it.group(
+ TextCodecs.CODEC.optionalFieldOf("header").forGetter(CurrentTabList::header),
+ TextCodecs.CODEC.optionalFieldOf("footer").forGetter(CurrentTabList::footer),
+ TextCodecs.CODEC.listOf().fieldOf("body").forGetter(CurrentTabList::body),
+ ).apply(it, ::CurrentTabList)
+ }
+ }
+ }
+
+ private fun getTabListNow(): CurrentTabList {
+ // This is a precondition for PlayerListHud.collectEntries to be valid
+ MC.networkHandler ?: return CurrentTabList(Optional.empty(), Optional.empty(), emptyList())
+ val hud = MC.inGameHud.playerListHud.cast()
+ val entries = hud.collectPlayerEntries_firmament()
+ .map {
+ it.displayName ?: run {
+ val team = it.scoreboardTeam
+ val name = it.profile.name
+ Team.decorateName(team, Text.literal(name))
+ }
+ }
+ return CurrentTabList(
+ header = hud.header_firmament.intoOptional(),
+ footer = hud.footer_firmament.intoOptional(),
+ body = entries,
+ )
+ }
+}
diff --git a/src/main/kotlin/util/mc/NbtPrism.kt b/src/main/kotlin/util/mc/NbtPrism.kt
new file mode 100644
index 0000000..f034210
--- /dev/null
+++ b/src/main/kotlin/util/mc/NbtPrism.kt
@@ -0,0 +1,91 @@
+package moe.nea.firmament.util.mc
+
+import com.google.gson.Gson
+import com.google.gson.JsonArray
+import com.google.gson.JsonElement
+import com.google.gson.JsonPrimitive
+import com.mojang.brigadier.StringReader
+import com.mojang.brigadier.arguments.ArgumentType
+import com.mojang.brigadier.arguments.StringArgumentType
+import com.mojang.brigadier.context.CommandContext
+import com.mojang.brigadier.suggestion.Suggestions
+import com.mojang.brigadier.suggestion.SuggestionsBuilder
+import com.mojang.serialization.JsonOps
+import java.util.concurrent.CompletableFuture
+import kotlin.collections.indices
+import kotlin.collections.map
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.nbt.NbtElement
+import net.minecraft.nbt.NbtList
+import net.minecraft.nbt.NbtOps
+import net.minecraft.nbt.NbtString
+import moe.nea.firmament.util.Base64Util
+
+class NbtPrism(val path: List<String>) {
+ companion object {
+ fun fromElement(path: JsonElement): NbtPrism? {
+ if (path is JsonArray) {
+ return NbtPrism(path.map { (it as JsonPrimitive).asString })
+ } else if (path is JsonPrimitive && path.isString) {
+ return NbtPrism(path.asString.split("."))
+ }
+ return null
+ }
+ }
+
+ object Argument : ArgumentType<NbtPrism> {
+ override fun parse(reader: StringReader): NbtPrism? {
+ return fromElement(JsonPrimitive(StringArgumentType.string().parse(reader)))
+ }
+
+ override fun getExamples(): Collection<String?>? {
+ return listOf("some.nbt.path", "some.other.*", "some.path.*json.in.a.json.string")
+ }
+ }
+
+ override fun toString(): String {
+ return "Prism($path)"
+ }
+
+ fun access(root: NbtElement): Collection<NbtElement> {
+ var rootSet = mutableListOf(root)
+ var switch = mutableListOf<NbtElement>()
+ for (pathSegment in path) {
+ if (pathSegment == ".") continue
+ if (pathSegment != "*" && pathSegment.startsWith("*")) {
+ if (pathSegment == "*json") {
+ for (element in rootSet) {
+ val eString = element.asString().getOrNull() ?: continue
+ val element = Gson().fromJson(eString, JsonElement::class.java)
+ switch.add(JsonOps.INSTANCE.convertTo(NbtOps.INSTANCE, element))
+ }
+ } else if (pathSegment == "*base64") {
+ for (element in rootSet) {
+ val string = element.asString().getOrNull() ?: continue
+ switch.add(NbtString.of(Base64Util.decodeString(string)))
+ }
+ }
+ }
+ for (element in rootSet) {
+ if (element is NbtList) {
+ if (pathSegment == "*")
+ switch.addAll(element)
+ val index = pathSegment.toIntOrNull() ?: continue
+ if (index !in element.indices) continue
+ switch.add(element[index])
+ }
+ if (element is NbtCompound) {
+ if (pathSegment == "*")
+ element.keys.mapTo(switch) { element.get(it)!! }
+ switch.add(element.get(pathSegment) ?: continue)
+ }
+ }
+ val temp = switch
+ switch = rootSet
+ rootSet = temp
+ switch.clear()
+ }
+ return rootSet
+ }
+}
diff --git a/src/main/kotlin/util/mc/NbtUtil.kt b/src/main/kotlin/util/mc/NbtUtil.kt
new file mode 100644
index 0000000..2cab1c7
--- /dev/null
+++ b/src/main/kotlin/util/mc/NbtUtil.kt
@@ -0,0 +1,10 @@
+package moe.nea.firmament.util.mc
+
+import net.minecraft.nbt.NbtElement
+import net.minecraft.nbt.NbtList
+
+fun Iterable<NbtElement>.toNbtList() = NbtList().also {
+ for (element in this) {
+ it.add(element)
+ }
+}
diff --git a/src/main/kotlin/util/mc/PlayerUtil.kt b/src/main/kotlin/util/mc/PlayerUtil.kt
new file mode 100644
index 0000000..53ef1f4
--- /dev/null
+++ b/src/main/kotlin/util/mc/PlayerUtil.kt
@@ -0,0 +1,7 @@
+package moe.nea.firmament.util.mc
+
+import net.minecraft.entity.EquipmentSlot
+import net.minecraft.entity.player.PlayerEntity
+
+
+val PlayerEntity.mainHandStack get() = this.getEquippedStack(EquipmentSlot.MAINHAND)
diff --git a/src/main/kotlin/util/mc/SNbtFormatter.kt b/src/main/kotlin/util/mc/SNbtFormatter.kt
index e773927..7617d17 100644
--- a/src/main/kotlin/util/mc/SNbtFormatter.kt
+++ b/src/main/kotlin/util/mc/SNbtFormatter.kt
@@ -1,5 +1,6 @@
package moe.nea.firmament.util.mc
+import net.minecraft.nbt.AbstractNbtList
import net.minecraft.nbt.NbtByte
import net.minecraft.nbt.NbtByteArray
import net.minecraft.nbt.NbtCompound
@@ -38,7 +39,7 @@ class SNbtFormatter private constructor() : NbtElementVisitor {
override fun visitString(element: NbtString) {
- result.append(NbtString.escape(element.asString()))
+ result.append(NbtString.escape(element.value))
}
override fun visitByte(element: NbtByte) {
@@ -65,18 +66,18 @@ class SNbtFormatter private constructor() : NbtElementVisitor {
result.append(element.doubleValue()).append("d")
}
- private fun visitArrayContents(array: List<NbtElement>) {
+ private fun visitArrayContents(array: AbstractNbtList) {
array.forEachIndexed { index, element ->
writeIndent()
element.accept(this)
- if (array.size != index + 1) {
+ if (array.size() != index + 1) {
result.append(",")
}
result.append("\n")
}
}
- private fun writeArray(arrayTypeTag: String, array: List<NbtElement>) {
+ private fun writeArray(arrayTypeTag: String, array: AbstractNbtList) {
result.append("[").append(arrayTypeTag).append("\n")
pushIndent()
visitArrayContents(array)
@@ -109,7 +110,7 @@ class SNbtFormatter private constructor() : NbtElementVisitor {
keys.forEachIndexed { index, key ->
writeIndent()
val element = compound[key] ?: error("Key '$key' found but not present in compound: $compound")
- val escapedName = if (key.matches(SIMPLE_NAME)) key else NbtString.escape(key)
+ val escapedName = escapeName(key)
result.append(escapedName).append(": ")
element.accept(this)
if (keys.size != index + 1) {
@@ -133,6 +134,9 @@ class SNbtFormatter private constructor() : NbtElementVisitor {
fun NbtElement.toPrettyString() = prettify(this)
- private val SIMPLE_NAME = "[A-Za-z0-9._+-]+".toRegex()
+ fun escapeName(key: String): String =
+ if (key.matches(SIMPLE_NAME)) key else NbtString.escape(key)
+
+ val SIMPLE_NAME = "[A-Za-z0-9._+-]+".toRegex()
}
}
diff --git a/src/main/kotlin/util/mc/SkullItemData.kt b/src/main/kotlin/util/mc/SkullItemData.kt
index 0405b65..1b7dcba 100644
--- a/src/main/kotlin/util/mc/SkullItemData.kt
+++ b/src/main/kotlin/util/mc/SkullItemData.kt
@@ -10,7 +10,6 @@ import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
-import kotlinx.serialization.encodeToString
import net.minecraft.component.DataComponentTypes
import net.minecraft.component.type.ProfileComponent
import net.minecraft.item.ItemStack
@@ -51,7 +50,7 @@ fun ItemStack.setEncodedSkullOwner(uuid: UUID, encodedData: String) {
this.set(DataComponentTypes.PROFILE, ProfileComponent(gameProfile))
}
-val zeroUUID = UUID.fromString("d3cb85e2-3075-48a1-b213-a9bfb62360c1")
+val arbitraryUUID = UUID.fromString("d3cb85e2-3075-48a1-b213-a9bfb62360c1")
fun createSkullItem(uuid: UUID, url: String) = ItemStack(Items.PLAYER_HEAD)
.also { it.setSkullOwner(uuid, url) }
diff --git a/src/main/kotlin/util/mc/SlotUtils.kt b/src/main/kotlin/util/mc/SlotUtils.kt
index 4709dcf..9eb4918 100644
--- a/src/main/kotlin/util/mc/SlotUtils.kt
+++ b/src/main/kotlin/util/mc/SlotUtils.kt
@@ -1,5 +1,6 @@
package moe.nea.firmament.util.mc
+import org.lwjgl.glfw.GLFW
import net.minecraft.screen.ScreenHandler
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
@@ -10,7 +11,7 @@ object SlotUtils {
MC.interactionManager?.clickSlot(
handler.syncId,
this.id,
- 2,
+ GLFW.GLFW_MOUSE_BUTTON_MIDDLE,
SlotActionType.CLONE,
MC.player
)
@@ -20,14 +21,25 @@ object SlotUtils {
MC.interactionManager?.clickSlot(
handler.syncId, this.id,
hotbarIndex, SlotActionType.SWAP,
- MC.player)
+ MC.player
+ )
}
fun Slot.clickRightMouseButton(handler: ScreenHandler) {
MC.interactionManager?.clickSlot(
handler.syncId,
this.id,
- 1,
+ GLFW.GLFW_MOUSE_BUTTON_RIGHT,
+ SlotActionType.PICKUP,
+ MC.player
+ )
+ }
+
+ fun Slot.clickLeftMouseButton(handler: ScreenHandler) {
+ MC.interactionManager?.clickSlot(
+ handler.syncId,
+ this.id,
+ GLFW.GLFW_MOUSE_BUTTON_LEFT,
SlotActionType.PICKUP,
MC.player
)
diff --git a/src/main/kotlin/util/mc/asFakeServer.kt b/src/main/kotlin/util/mc/asFakeServer.kt
new file mode 100644
index 0000000..d3811bd
--- /dev/null
+++ b/src/main/kotlin/util/mc/asFakeServer.kt
@@ -0,0 +1,37 @@
+package moe.nea.firmament.util.mc
+
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
+import net.minecraft.server.command.CommandOutput
+import net.minecraft.server.command.ServerCommandSource
+import net.minecraft.text.Text
+
+fun FabricClientCommandSource.asFakeServer(): ServerCommandSource {
+ val source = this
+ return ServerCommandSource(
+ object : CommandOutput {
+ override fun sendMessage(message: Text?) {
+ source.player.sendMessage(message, false)
+ }
+
+ override fun shouldReceiveFeedback(): Boolean {
+ return true
+ }
+
+ override fun shouldTrackOutput(): Boolean {
+ return true
+ }
+
+ override fun shouldBroadcastConsoleToOps(): Boolean {
+ return true
+ }
+ },
+ source.position,
+ source.rotation,
+ null,
+ 0,
+ "FakeServerCommandSource",
+ Text.literal("FakeServerCommandSource"),
+ null,
+ source.player
+ )
+}
diff --git a/src/main/kotlin/util/regex.kt b/src/main/kotlin/util/regex.kt
index f239810..be6bcfb 100644
--- a/src/main/kotlin/util/regex.kt
+++ b/src/main/kotlin/util/regex.kt
@@ -26,6 +26,13 @@ inline fun <T> Pattern.useMatch(string: String?, block: Matcher.() -> T): T? {
?.let(block)
}
+fun <T> String.ifDropLast(suffix: String, block: (String) -> T): T? {
+ if (endsWith(suffix)) {
+ return block(dropLast(suffix.length))
+ }
+ return null
+}
+
@Language("RegExp")
val TIME_PATTERN = "[0-9]+[ms]"
diff --git a/src/main/kotlin/util/render/CustomRenderLayers.kt b/src/main/kotlin/util/render/CustomRenderLayers.kt
new file mode 100644
index 0000000..3d9e598
--- /dev/null
+++ b/src/main/kotlin/util/render/CustomRenderLayers.kt
@@ -0,0 +1,104 @@
+package util.render
+
+import com.mojang.blaze3d.pipeline.BlendFunction
+import com.mojang.blaze3d.pipeline.RenderPipeline
+import com.mojang.blaze3d.platform.DepthTestFunction
+import com.mojang.blaze3d.vertex.VertexFormat.DrawMode
+import java.util.function.Function
+import net.minecraft.client.gl.RenderPipelines
+import net.minecraft.client.gl.UniformType
+import net.minecraft.client.render.RenderLayer
+import net.minecraft.client.render.RenderPhase
+import net.minecraft.client.render.VertexFormats
+import net.minecraft.util.Identifier
+import net.minecraft.util.TriState
+import net.minecraft.util.Util
+import moe.nea.firmament.Firmament
+
+object CustomRenderPipelines {
+ val GUI_TEXTURED_NO_DEPTH_TRIS =
+ RenderPipeline.builder(RenderPipelines.POSITION_TEX_COLOR_SNIPPET)
+ .withVertexFormat(VertexFormats.POSITION_TEXTURE_COLOR, DrawMode.TRIANGLES)
+ .withLocation(Firmament.identifier("gui_textured_overlay_tris"))
+ .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
+ .withCull(false)
+ .withDepthWrite(false)
+ .build()
+ val OMNIPRESENT_LINES = RenderPipeline
+ .builder(RenderPipelines.RENDERTYPE_LINES_SNIPPET)
+ .withLocation(Firmament.identifier("lines"))
+ .withDepthWrite(false)
+ .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
+ .build()
+ val COLORED_OMNIPRESENT_QUADS =
+ RenderPipeline.builder(RenderPipelines.MATRICES_COLOR_SNIPPET)// TODO: split this up to support better transparent ordering.
+ .withLocation(Firmament.identifier("colored_omnipresent_quads"))
+ .withVertexShader("core/position_color")
+ .withFragmentShader("core/position_color")
+ .withVertexFormat(VertexFormats.POSITION_COLOR, DrawMode.QUADS)
+ .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
+ .withCull(false)
+ .withDepthWrite(false)
+ .build()
+
+ val CIRCLE_FILTER_TRANSLUCENT_GUI_TRIS =
+ RenderPipeline.builder(RenderPipelines.POSITION_TEX_COLOR_SNIPPET)
+ .withVertexFormat(VertexFormats.POSITION_TEXTURE_COLOR, DrawMode.TRIANGLES)
+ .withLocation(Firmament.identifier("gui_textured_overlay_tris_circle"))
+ .withUniform("InnerCutoutRadius", UniformType.FLOAT)
+ .withFragmentShader(Firmament.identifier("circle_discard_color"))
+ .withBlend(BlendFunction.TRANSLUCENT)
+ .build()
+ val PARALLAX_CAPE_SHADER =
+ RenderPipeline.builder(RenderPipelines.ENTITY_SNIPPET)
+ .withLocation(Firmament.identifier("parallax_cape"))
+ .withFragmentShader(Firmament.identifier("cape/parallax"))
+ .withSampler("Sampler0")
+ .withSampler("Sampler1")
+ .withSampler("Sampler3")
+ .withUniform("Animation", UniformType.FLOAT)
+ .build()
+}
+
+object CustomRenderLayers {
+ inline fun memoizeTextured(crossinline func: (Identifier) -> RenderLayer) = memoize(func)
+ inline fun <T, R> memoize(crossinline func: (T) -> R): Function<T, R> {
+ return Util.memoize { it: T -> func(it) }
+ }
+
+ val GUI_TEXTURED_NO_DEPTH_TRIS = memoizeTextured { texture ->
+ RenderLayer.of(
+ "firmament_gui_textured_overlay_tris",
+ RenderLayer.DEFAULT_BUFFER_SIZE,
+ CustomRenderPipelines.GUI_TEXTURED_NO_DEPTH_TRIS,
+ RenderLayer.MultiPhaseParameters.builder().texture(
+ RenderPhase.Texture(texture, TriState.DEFAULT, false)
+ )
+ .build(false)
+ )
+ }
+ val LINES = RenderLayer.of(
+ "firmament_lines",
+ RenderLayer.DEFAULT_BUFFER_SIZE,
+ CustomRenderPipelines.OMNIPRESENT_LINES,
+ RenderLayer.MultiPhaseParameters.builder() // TODO: accept linewidth here
+ .build(false)
+ )
+ val COLORED_QUADS = RenderLayer.of(
+ "firmament_quads",
+ RenderLayer.DEFAULT_BUFFER_SIZE,
+ CustomRenderPipelines.COLORED_OMNIPRESENT_QUADS,
+ RenderLayer.MultiPhaseParameters.builder()
+ .lightmap(RenderPhase.DISABLE_LIGHTMAP)
+ .build(false)
+ )
+
+ val TRANSLUCENT_CIRCLE_GUI =
+ RenderLayer.of(
+ "firmament_circle_gui",
+ RenderLayer.DEFAULT_BUFFER_SIZE,
+ CustomRenderPipelines.CIRCLE_FILTER_TRANSLUCENT_GUI_TRIS,
+ RenderLayer.MultiPhaseParameters.builder()
+ .build(false)
+ )
+}
diff --git a/src/main/kotlin/util/render/DrawContextExt.kt b/src/main/kotlin/util/render/DrawContextExt.kt
index a143d4d..a833c86 100644
--- a/src/main/kotlin/util/render/DrawContextExt.kt
+++ b/src/main/kotlin/util/render/DrawContextExt.kt
@@ -3,50 +3,16 @@ package moe.nea.firmament.util.render
import com.mojang.blaze3d.systems.RenderSystem
import me.shedaniel.math.Color
import org.joml.Matrix4f
+import util.render.CustomRenderLayers
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.render.RenderLayer
-import net.minecraft.client.render.RenderLayer.MultiPhaseParameters
-import net.minecraft.client.render.RenderPhase
-import net.minecraft.client.render.VertexFormat
-import net.minecraft.client.render.VertexFormat.DrawMode
-import net.minecraft.client.render.VertexFormats
import net.minecraft.util.Identifier
-import net.minecraft.util.TriState
-import net.minecraft.util.Util
import moe.nea.firmament.util.MC
fun DrawContext.isUntranslatedGuiDrawContext(): Boolean {
return (matrices.peek().positionMatrix.properties() and Matrix4f.PROPERTY_IDENTITY.toInt()) != 0
}
-object GuiRenderLayers {
- val GUI_TEXTURED_NO_DEPTH = Util.memoize<Identifier, RenderLayer> { texture: Identifier ->
- RenderLayer.of("firmament_gui_textured_no_depth",
- VertexFormats.POSITION_TEXTURE_COLOR,
- DrawMode.QUADS,
- DEFAULT_BUFFER_SIZE,
- MultiPhaseParameters.builder()
- .texture(RenderPhase.Texture(texture, TriState.FALSE, false))
- .program(RenderPhase.POSITION_TEXTURE_COLOR_PROGRAM)
- .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY)
- .depthTest(RenderPhase.ALWAYS_DEPTH_TEST)
- .build(false))
- }
- val GUI_TEXTURED_TRIS = Util.memoize { texture: Identifier ->
- RenderLayer.of("firmament_gui_textured_overlay_tris",
- VertexFormats.POSITION_TEXTURE_COLOR,
- DrawMode.TRIANGLES,
- DEFAULT_BUFFER_SIZE,
- MultiPhaseParameters.builder()
- .texture(RenderPhase.Texture(texture, TriState.DEFAULT, false))
- .program(RenderPhase.POSITION_TEXTURE_COLOR_PROGRAM)
- .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY)
- .depthTest(RenderPhase.ALWAYS_DEPTH_TEST)
- .writeMaskState(RenderPhase.COLOR_MASK)
- .build(false))
- }
-}
-
@Deprecated("Use the other drawGuiTexture")
fun DrawContext.drawGuiTexture(
x: Int, y: Int, z: Int, width: Int, height: Int, sprite: Identifier
@@ -91,10 +57,11 @@ fun DrawContext.drawLine(fromX: Int, fromY: Int, toX: Int, toY: Int, color: Colo
}
RenderSystem.lineWidth(MC.window.scaleFactor.toFloat())
draw { vertexConsumers ->
- val buf = vertexConsumers.getBuffer(RenderInWorldContext.RenderLayers.LINES)
- buf.vertex(fromX.toFloat(), fromY.toFloat(), 0F).color(color.color)
+ val buf = vertexConsumers.getBuffer(CustomRenderLayers.LINES)
+ val matrix = this.matrices.peek()
+ buf.vertex(matrix, fromX.toFloat(), fromY.toFloat(), 0F).color(color.color)
.normal(toX - fromX.toFloat(), toY - fromY.toFloat(), 0F)
- buf.vertex(toX.toFloat(), toY.toFloat(), 0F).color(color.color)
+ buf.vertex(matrix, toX.toFloat(), toY.toFloat(), 0F).color(color.color)
.normal(toX - fromX.toFloat(), toY - fromY.toFloat(), 0F)
}
}
diff --git a/src/main/kotlin/util/render/FacingThePlayerContext.kt b/src/main/kotlin/util/render/FacingThePlayerContext.kt
index daa8da9..670beb6 100644
--- a/src/main/kotlin/util/render/FacingThePlayerContext.kt
+++ b/src/main/kotlin/util/render/FacingThePlayerContext.kt
@@ -1,18 +1,12 @@
package moe.nea.firmament.util.render
-import com.mojang.blaze3d.systems.RenderSystem
import io.github.notenoughupdates.moulconfig.platform.next
import org.joml.Matrix4f
import net.minecraft.client.font.TextRenderer
-import net.minecraft.client.render.BufferRenderer
-import net.minecraft.client.render.GameRenderer
import net.minecraft.client.render.LightmapTextureManager
import net.minecraft.client.render.RenderLayer
-import net.minecraft.client.render.Tessellator
import net.minecraft.client.render.VertexConsumer
-import net.minecraft.client.render.VertexFormat
-import net.minecraft.client.render.VertexFormats
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
diff --git a/src/main/kotlin/util/render/FirmamentShaders.kt b/src/main/kotlin/util/render/FirmamentShaders.kt
index ba67dbb..cc6cd49 100644
--- a/src/main/kotlin/util/render/FirmamentShaders.kt
+++ b/src/main/kotlin/util/render/FirmamentShaders.kt
@@ -1,9 +1,10 @@
package moe.nea.firmament.util.render
+import com.mojang.blaze3d.vertex.VertexFormat
+import net.minecraft.client.gl.CompiledShader
import net.minecraft.client.gl.Defines
-import net.minecraft.client.gl.ShaderProgramKey
+import net.minecraft.client.gl.ShaderProgram
import net.minecraft.client.render.RenderPhase
-import net.minecraft.client.render.VertexFormat
import net.minecraft.client.render.VertexFormats
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
@@ -11,20 +12,9 @@ import moe.nea.firmament.events.DebugInstantiateEvent
import moe.nea.firmament.util.MC
object FirmamentShaders {
- val shaders = mutableListOf<ShaderProgramKey>()
-
- private fun shader(name: String, format: VertexFormat, defines: Defines): ShaderProgramKey {
- val key = ShaderProgramKey(Firmament.identifier(name), format, defines)
- shaders.add(key)
- return key
- }
-
- val LINES = RenderPhase.ShaderProgram(shader("core/rendertype_lines", VertexFormats.LINES, Defines.EMPTY))
@Subscribe
fun debugLoad(event: DebugInstantiateEvent) {
- shaders.forEach {
- MC.instance.shaderLoader.getOrCreateProgram(it)
- }
+ // TODO: do i still need to work with shaders like this?
}
}
diff --git a/src/main/kotlin/util/render/LerpUtils.kt b/src/main/kotlin/util/render/LerpUtils.kt
index f2c2f25..63a13ec 100644
--- a/src/main/kotlin/util/render/LerpUtils.kt
+++ b/src/main/kotlin/util/render/LerpUtils.kt
@@ -1,33 +1,36 @@
-
package moe.nea.firmament.util.render
import me.shedaniel.math.Color
-val pi = Math.PI
-val tau = Math.PI * 2
-fun lerpAngle(a: Float, b: Float, progress: Float): Float {
- // TODO: there is at least 10 mods to many in here lol
- val shortestAngle = ((((b.mod(tau) - a.mod(tau)).mod(tau)) + tau + pi).mod(tau)) - pi
- return ((a + (shortestAngle) * progress).mod(tau)).toFloat()
+val π = Math.PI
+val τ = Math.PI * 2
+fun lerpAngle(a: Float, b: Float, progress: Float): Float {
+ // TODO: there is at least 10 mods to many in here lol
+ val shortestAngle = ((((b.mod(τ) - a.mod(τ)).mod(τ)) + τ + π).mod(τ)) - π
+ return ((a + (shortestAngle) * progress).mod(τ)).toFloat()
}
+fun wrapAngle(angle: Float): Float = (angle.mod(τ) + τ).mod(τ).toFloat()
+fun wrapAngle(angle: Double): Double = (angle.mod(τ) + τ).mod(τ)
+
fun lerp(a: Float, b: Float, progress: Float): Float {
- return a + (b - a) * progress
+ return a + (b - a) * progress
}
+
fun lerp(a: Int, b: Int, progress: Float): Int {
- return (a + (b - a) * progress).toInt()
+ return (a + (b - a) * progress).toInt()
}
fun ilerp(a: Float, b: Float, value: Float): Float {
- return (value - a) / (b - a)
+ return (value - a) / (b - a)
}
fun lerp(a: Color, b: Color, progress: Float): Color {
- return Color.ofRGBA(
- lerp(a.red, b.red, progress),
- lerp(a.green, b.green, progress),
- lerp(a.blue, b.blue, progress),
- lerp(a.alpha, b.alpha, progress),
- )
+ return Color.ofRGBA(
+ lerp(a.red, b.red, progress),
+ lerp(a.green, b.green, progress),
+ lerp(a.blue, b.blue, progress),
+ lerp(a.alpha, b.alpha, progress),
+ )
}
diff --git a/src/main/kotlin/util/render/RenderCircleProgress.kt b/src/main/kotlin/util/render/RenderCircleProgress.kt
index 805633c..81dde6f 100644
--- a/src/main/kotlin/util/render/RenderCircleProgress.kt
+++ b/src/main/kotlin/util/render/RenderCircleProgress.kt
@@ -1,93 +1,101 @@
package moe.nea.firmament.util.render
import com.mojang.blaze3d.systems.RenderSystem
+import com.mojang.blaze3d.vertex.VertexFormat
import io.github.notenoughupdates.moulconfig.platform.next
+import java.util.OptionalInt
import org.joml.Matrix4f
-import org.joml.Vector2f
-import kotlin.math.atan2
-import kotlin.math.tan
+import util.render.CustomRenderLayers
import net.minecraft.client.gui.DrawContext
-import net.minecraft.client.render.BufferRenderer
+import net.minecraft.client.render.BufferBuilder
import net.minecraft.client.render.RenderLayer
-import net.minecraft.client.render.RenderPhase
-import net.minecraft.client.render.Tessellator
-import net.minecraft.client.render.VertexFormat.DrawMode
-import net.minecraft.client.render.VertexFormats
+import net.minecraft.client.util.BufferAllocator
import net.minecraft.util.Identifier
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.collections.nonNegligibleSubSectionsAlignedWith
+import moe.nea.firmament.util.math.Projections
object RenderCircleProgress {
- fun renderCircle(
+ fun renderCircularSlice(
drawContext: DrawContext,
- texture: Identifier,
- progress: Float,
+ layer: RenderLayer,
u1: Float,
u2: Float,
v1: Float,
v2: Float,
+ angleRadians: ClosedFloatingPointRange<Float>,
+ color: Int = -1,
+ innerCutoutRadius: Float = 0F
) {
- RenderSystem.enableBlend()
- drawContext.draw {
- val bufferBuilder = it.getBuffer(GuiRenderLayers.GUI_TEXTURED_TRIS.apply(texture))
- val matrix: Matrix4f = drawContext.matrices.peek().positionMatrix
-
- val corners = listOf(
- Vector2f(0F, -1F),
- Vector2f(1F, -1F),
- Vector2f(1F, 0F),
- Vector2f(1F, 1F),
- Vector2f(0F, 1F),
- Vector2f(-1F, 1F),
- Vector2f(-1F, 0F),
- Vector2f(-1F, -1F),
- )
+ drawContext.draw()
+ val sections = angleRadians.nonNegligibleSubSectionsAlignedWith((τ / 8f).toFloat())
+ .zipWithNext().toList()
+ BufferAllocator(layer.vertexFormat.vertexSize * sections.size * 3).use { allocator ->
- for (i in (0 until 8)) {
- if (progress < i / 8F) {
- break
- }
- val second = corners[(i + 1) % 8]
- val first = corners[i]
- if (progress <= (i + 1) / 8F) {
- val internalProgress = 1 - (progress - i / 8F) * 8F
- val angle = lerpAngle(
- atan2(second.y, second.x),
- atan2(first.y, first.x),
- internalProgress
- )
- if (angle < tau / 8 || angle >= tau * 7 / 8) {
- second.set(1F, tan(angle))
- } else if (angle < tau * 3 / 8) {
- second.set(1 / tan(angle), 1F)
- } else if (angle < tau * 5 / 8) {
- second.set(-1F, -tan(angle))
- } else {
- second.set(-1 / tan(angle), -1F)
- }
- }
+ val bufferBuilder = BufferBuilder(allocator, VertexFormat.DrawMode.TRIANGLES, layer.vertexFormat)
+ val matrix: Matrix4f = drawContext.matrices.peek().positionMatrix
+ for ((sectionStart, sectionEnd) in sections) {
+ val firstPoint = Projections.Two.projectAngleOntoUnitBox(sectionStart.toDouble())
+ val secondPoint = Projections.Two.projectAngleOntoUnitBox(sectionEnd.toDouble())
fun ilerp(f: Float): Float =
ilerp(-1f, 1f, f)
bufferBuilder
- .vertex(matrix, second.x, second.y, 0F)
- .texture(lerp(u1, u2, ilerp(second.x)), lerp(v1, v2, ilerp(second.y)))
- .color(-1)
+ .vertex(matrix, secondPoint.x, secondPoint.y, 0F)
+ .texture(lerp(u1, u2, ilerp(secondPoint.x)), lerp(v1, v2, ilerp(secondPoint.y)))
+ .color(color)
.next()
bufferBuilder
- .vertex(matrix, first.x, first.y, 0F)
- .texture(lerp(u1, u2, ilerp(first.x)), lerp(v1, v2, ilerp(first.y)))
- .color(-1)
+ .vertex(matrix, firstPoint.x, firstPoint.y, 0F)
+ .texture(lerp(u1, u2, ilerp(firstPoint.x)), lerp(v1, v2, ilerp(firstPoint.y)))
+ .color(color)
.next()
bufferBuilder
.vertex(matrix, 0F, 0F, 0F)
.texture(lerp(u1, u2, ilerp(0F)), lerp(v1, v2, ilerp(0F)))
- .color(-1)
+ .color(color)
.next()
}
+
+ bufferBuilder.end().use { buffer ->
+ // TODO: write a better utility to pass uniforms :sob: ill even take a mixin at this point
+ if (innerCutoutRadius <= 0) {
+ layer.draw(buffer)
+ return
+ }
+ val vertexBuffer = layer.vertexFormat.uploadImmediateVertexBuffer(buffer.buffer)
+ val indexBufferConstructor = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.TRIANGLES)
+ val indexBuffer = indexBufferConstructor.getIndexBuffer(buffer.drawParameters.indexCount)
+ RenderSystem.getDevice().createCommandEncoder().createRenderPass(
+ MC.instance.framebuffer.colorAttachment,
+ OptionalInt.empty(),
+ ).use { renderPass ->
+ renderPass.setPipeline(layer.pipeline)
+ renderPass.setUniform("InnerCutoutRadius", innerCutoutRadius)
+ renderPass.setIndexBuffer(indexBuffer, indexBufferConstructor.indexType)
+ renderPass.setVertexBuffer(0, vertexBuffer)
+ renderPass.drawIndexed(0, buffer.drawParameters.indexCount)
+ }
+ }
}
- RenderSystem.disableBlend()
}
-
+ fun renderCircle(
+ drawContext: DrawContext,
+ texture: Identifier,
+ progress: Float,
+ u1: Float,
+ u2: Float,
+ v1: Float,
+ v2: Float,
+ ) {
+ renderCircularSlice(
+ drawContext,
+ CustomRenderLayers.GUI_TEXTURED_NO_DEPTH_TRIS.apply(texture),
+ u1, u2, v1, v2,
+ (-τ / 4).toFloat()..(progress * τ - τ / 4).toFloat()
+ )
+ }
}
diff --git a/src/main/kotlin/util/render/RenderInWorldContext.kt b/src/main/kotlin/util/render/RenderInWorldContext.kt
index bb58200..98b10ca 100644
--- a/src/main/kotlin/util/render/RenderInWorldContext.kt
+++ b/src/main/kotlin/util/render/RenderInWorldContext.kt
@@ -5,15 +5,12 @@ import io.github.notenoughupdates.moulconfig.platform.next
import java.lang.Math.pow
import org.joml.Matrix4f
import org.joml.Vector3f
-import net.minecraft.client.gl.VertexBuffer
+import util.render.CustomRenderLayers
import net.minecraft.client.render.Camera
import net.minecraft.client.render.RenderLayer
-import net.minecraft.client.render.RenderPhase
import net.minecraft.client.render.RenderTickCounter
-import net.minecraft.client.render.Tessellator
import net.minecraft.client.render.VertexConsumer
import net.minecraft.client.render.VertexConsumerProvider
-import net.minecraft.client.render.VertexFormat
import net.minecraft.client.render.VertexFormats
import net.minecraft.client.texture.Sprite
import net.minecraft.client.util.math.MatrixStack
@@ -27,47 +24,12 @@ import moe.nea.firmament.util.MC
@RenderContextDSL
class RenderInWorldContext private constructor(
- private val tesselator: Tessellator,
val matrixStack: MatrixStack,
private val camera: Camera,
private val tickCounter: RenderTickCounter,
val vertexConsumers: VertexConsumerProvider.Immediate,
) {
- object RenderLayers {
- val TRANSLUCENT_TRIS = RenderLayer.of("firmament_translucent_tris",
- VertexFormats.POSITION_COLOR,
- VertexFormat.DrawMode.TRIANGLES,
- RenderLayer.CUTOUT_BUFFER_SIZE,
- false, true,
- RenderLayer.MultiPhaseParameters.builder()
- .depthTest(RenderPhase.ALWAYS_DEPTH_TEST)
- .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY)
- .program(RenderPhase.POSITION_COLOR_PROGRAM)
- .build(false))
- val LINES = RenderLayer.of("firmament_rendertype_lines",
- VertexFormats.LINES,
- VertexFormat.DrawMode.LINES,
- RenderLayer.CUTOUT_BUFFER_SIZE,
- false, false, // do we need translucent? i dont think so
- RenderLayer.MultiPhaseParameters.builder()
- .depthTest(RenderPhase.ALWAYS_DEPTH_TEST)
- .program(FirmamentShaders.LINES)
- .build(false)
- )
- val COLORED_QUADS = RenderLayer.of(
- "firmament_quads",
- VertexFormats.POSITION_COLOR,
- VertexFormat.DrawMode.QUADS,
- RenderLayer.CUTOUT_BUFFER_SIZE,
- false, true,
- RenderLayer.MultiPhaseParameters.builder()
- .depthTest(RenderPhase.ALWAYS_DEPTH_TEST)
- .program(RenderPhase.POSITION_COLOR_PROGRAM)
- .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY)
- .build(false)
- )
- }
@Deprecated("stateful color management is no longer a thing")
fun color(color: me.shedaniel.math.Color) {
@@ -82,7 +44,7 @@ class RenderInWorldContext private constructor(
fun block(blockPos: BlockPos, color: Int) {
matrixStack.push()
matrixStack.translate(blockPos.x.toFloat(), blockPos.y.toFloat(), blockPos.z.toFloat())
- buildCube(matrixStack.peek().positionMatrix, vertexConsumers.getBuffer(RenderLayers.COLORED_QUADS), color)
+ buildCube(matrixStack.peek().positionMatrix, vertexConsumers.getBuffer(CustomRenderLayers.COLORED_QUADS), color)
matrixStack.pop()
}
@@ -155,7 +117,7 @@ class RenderInWorldContext private constructor(
matrixStack.translate(vec3d.x, vec3d.y, vec3d.z)
matrixStack.scale(size, size, size)
matrixStack.translate(-.5, -.5, -.5)
- buildCube(matrixStack.peek().positionMatrix, vertexConsumers.getBuffer(RenderLayers.COLORED_QUADS), color)
+ buildCube(matrixStack.peek().positionMatrix, vertexConsumers.getBuffer(CustomRenderLayers.COLORED_QUADS), color)
matrixStack.pop()
vertexConsumers.draw()
}
@@ -182,8 +144,7 @@ class RenderInWorldContext private constructor(
fun line(points: List<Vec3d>, lineWidth: Float = 10F) {
RenderSystem.lineWidth(lineWidth)
- // TODO: replace with renderlayers
- val buffer = tesselator.begin(VertexFormat.DrawMode.LINES, VertexFormats.LINES)
+ val buffer = vertexConsumers.getBuffer(CustomRenderLayers.LINES)
val matrix = matrixStack.peek()
var lastNormal: Vector3f? = null
@@ -203,7 +164,6 @@ class RenderInWorldContext private constructor(
.next()
}
- RenderLayers.LINES.draw(buffer.end())
}
// TODO: put the favourite icons in front of items again
@@ -281,16 +241,15 @@ class RenderInWorldContext private constructor(
fun renderInWorld(event: WorldRenderLastEvent, block: RenderInWorldContext. () -> Unit) {
// TODO: there should be *no more global state*. the only thing we should be doing is render layers. that includes settings like culling, blending, shader color, and depth testing
// For now i will let these functions remain, but this needs to go before i do a full (non-beta) release
- RenderSystem.disableDepthTest()
- RenderSystem.enableBlend()
- RenderSystem.defaultBlendFunc()
- RenderSystem.disableCull()
+// RenderSystem.disableDepthTest()
+// RenderSystem.enableBlend()
+// RenderSystem.defaultBlendFunc()
+// RenderSystem.disableCull()
event.matrices.push()
event.matrices.translate(-event.camera.pos.x, -event.camera.pos.y, -event.camera.pos.z)
val ctx = RenderInWorldContext(
- RenderSystem.renderThreadTesselator(),
event.matrices,
event.camera,
event.tickCounter,
@@ -302,10 +261,6 @@ class RenderInWorldContext private constructor(
event.matrices.pop()
event.vertexConsumers.draw()
RenderSystem.setShaderColor(1F, 1F, 1F, 1F)
- VertexBuffer.unbind()
- RenderSystem.enableDepthTest()
- RenderSystem.enableCull()
- RenderSystem.disableBlend()
}
}
}
diff --git a/src/main/kotlin/util/render/TintedOverlayTexture.kt b/src/main/kotlin/util/render/TintedOverlayTexture.kt
index a02eccc..0677846 100644
--- a/src/main/kotlin/util/render/TintedOverlayTexture.kt
+++ b/src/main/kotlin/util/render/TintedOverlayTexture.kt
@@ -1,7 +1,5 @@
package moe.nea.firmament.util.render
-import com.mojang.blaze3d.platform.GlConst
-import com.mojang.blaze3d.systems.RenderSystem
import me.shedaniel.math.Color
import net.minecraft.client.render.OverlayTexture
import net.minecraft.util.math.ColorHelper
@@ -29,16 +27,9 @@ class TintedOverlayTexture : OverlayTexture() {
}
}
- RenderSystem.activeTexture(GlConst.GL_TEXTURE1)
- texture.bindTexture()
texture.setFilter(false, false)
texture.setClamp(true)
- image.upload(0,
- 0, 0,
- 0, 0,
- image.width, image.height,
- false)
- RenderSystem.activeTexture(GlConst.GL_TEXTURE0)
+ texture.upload()
return this
}
}
diff --git a/src/main/kotlin/util/skyblock/SackUtil.kt b/src/main/kotlin/util/skyblock/SackUtil.kt
index fd67c44..c46542e 100644
--- a/src/main/kotlin/util/skyblock/SackUtil.kt
+++ b/src/main/kotlin/util/skyblock/SackUtil.kt
@@ -93,7 +93,7 @@ object SackUtil {
fun updateFromHoverText(text: Text) {
text.siblings.forEach(::updateFromHoverText)
- val hoverText = text.style.hoverEvent?.getValue(HoverEvent.Action.SHOW_TEXT) ?: return
+ val hoverText = (text.style.hoverEvent as? HoverEvent.ShowText)?.value ?: return
val cleanedText = hoverText.unformattedString
if (cleanedText.startsWith("Added items:\n")) {
if (!foundAdded) {
diff --git a/src/main/kotlin/util/skyblock/SkyBlockItems.kt b/src/main/kotlin/util/skyblock/SkyBlockItems.kt
index ca2b17b..9854be0 100644
--- a/src/main/kotlin/util/skyblock/SkyBlockItems.kt
+++ b/src/main/kotlin/util/skyblock/SkyBlockItems.kt
@@ -3,6 +3,7 @@ package moe.nea.firmament.util.skyblock
import moe.nea.firmament.util.SkyblockId
object SkyBlockItems {
+ val COINS = SkyblockId("SKYBLOCK_COIN")
val ROTTEN_FLESH = SkyblockId("ROTTEN_FLESH")
val ENCHANTED_DIAMOND = SkyblockId("ENCHANTED_DIAMOND")
val DIAMOND = SkyblockId("DIAMOND")
@@ -13,4 +14,6 @@ object SkyBlockItems {
val SLICE_OF_GREEN_VELVET_CAKE = SkyblockId("SLICE_OF_GREEN_VELVET_CAKE")
val SLICE_OF_RED_VELVET_CAKE = SkyblockId("SLICE_OF_RED_VELVET_CAKE")
val SLICE_OF_STRAWBERRY_SHORTCAKE = SkyblockId("SLICE_OF_STRAWBERRY_SHORTCAKE")
+ val ASPECT_OF_THE_VOID = SkyblockId("ASPECT_OF_THE_VOID")
+ val ASPECT_OF_THE_END = SkyblockId("ASPECT_OF_THE_END")
}
diff --git a/src/main/kotlin/util/skyblock/TabListAPI.kt b/src/main/kotlin/util/skyblock/TabListAPI.kt
new file mode 100644
index 0000000..6b937da
--- /dev/null
+++ b/src/main/kotlin/util/skyblock/TabListAPI.kt
@@ -0,0 +1,41 @@
+package moe.nea.firmament.util.skyblock
+
+import org.intellij.lang.annotations.Language
+import net.minecraft.text.Text
+import moe.nea.firmament.util.StringUtil.title
+import moe.nea.firmament.util.StringUtil.unwords
+import moe.nea.firmament.util.mc.MCTabListAPI
+import moe.nea.firmament.util.unformattedString
+
+object TabListAPI {
+
+ fun getWidgetLines(widgetName: WidgetName, includeTitle: Boolean = false, from: MCTabListAPI.CurrentTabList = MCTabListAPI.currentTabList): List<Text> {
+ return from.body
+ .dropWhile { !widgetName.matchesTitle(it) }
+ .takeWhile { it.string.isNotBlank() && !it.string.startsWith(" ") }
+ .let { if (includeTitle) it else it.drop(1) }
+ }
+
+ enum class WidgetName(regex: Regex?) {
+ COMMISSIONS,
+ SKILLS("Skills:( .*)?"),
+ PROFILE("Profile: (.*)"),
+ COLLECTION,
+ ESSENCE,
+ PET
+ ;
+
+ fun matchesTitle(it: Text): Boolean {
+ return regex.matches(it.unformattedString)
+ }
+
+ constructor() : this(null)
+ constructor(@Language("RegExp") regex: String) : this(Regex(regex))
+
+ val label =
+ name.split("_").map { it.lowercase().title() }.unwords()
+ val regex = regex ?: Regex.fromLiteral("$label:")
+
+ }
+
+}
diff --git a/src/main/kotlin/util/textutil.kt b/src/main/kotlin/util/textutil.kt
index 806f61e..cfda2e9 100644
--- a/src/main/kotlin/util/textutil.kt
+++ b/src/main/kotlin/util/textutil.kt
@@ -56,6 +56,7 @@ fun OrderedText.reconstitute(): MutableText {
return base
}
+
fun StringVisitable.reconstitute(): MutableText {
val base = Text.literal("")
base.setStyle(Style.EMPTY.withItalic(false))
@@ -82,15 +83,47 @@ val Text.unformattedString: String
val Text.directLiteralStringContent: String? get() = (this.content as? PlainTextContent)?.string()
-fun Text.getLegacyFormatString() =
+fun Text.getLegacyFormatString(trimmed: Boolean = false): String =
run {
+ var lastCode = "§r"
val sb = StringBuilder()
+ fun appendCode(code: String) {
+ if (code != lastCode || !trimmed) {
+ sb.append(code)
+ lastCode = code
+ }
+ }
for (component in iterator()) {
- sb.append(component.style.color?.toChatFormatting()?.toString() ?: "§r")
+ if (component.directLiteralStringContent.isNullOrEmpty() && component.siblings.isEmpty()) {
+ continue
+ }
+ appendCode(component.style.let { style ->
+ var color = style.color?.toChatFormatting()?.toString() ?: "§r"
+ if (style.isBold)
+ color += LegacyFormattingCode.BOLD.formattingCode
+ if (style.isItalic)
+ color += LegacyFormattingCode.ITALIC.formattingCode
+ if (style.isUnderlined)
+ color += LegacyFormattingCode.UNDERLINE.formattingCode
+ if (style.isObfuscated)
+ color += LegacyFormattingCode.OBFUSCATED.formattingCode
+ if (style.isStrikethrough)
+ color += LegacyFormattingCode.STRIKETHROUGH.formattingCode
+ color
+ })
sb.append(component.directLiteralStringContent)
- sb.append("§r")
+ if (!trimmed)
+ appendCode("§r")
}
sb.toString()
+ }.also {
+ var it = it
+ if (trimmed) {
+ it = it.removeSuffix("§r")
+ if (it.length == 2 && it.startsWith("§"))
+ it = ""
+ }
+ it
}
private val textColorLUT = Formatting.entries
@@ -127,13 +160,13 @@ fun MutableText.darkGrey() = withColor(Formatting.DARK_GRAY)
fun MutableText.red() = withColor(Formatting.RED)
fun MutableText.white() = withColor(Formatting.WHITE)
fun MutableText.bold(): MutableText = styled { it.withBold(true) }
-fun MutableText.hover(text: Text): MutableText = styled {it.withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, text))}
+fun MutableText.hover(text: Text): MutableText = styled { it.withHoverEvent(HoverEvent.ShowText(text)) }
fun MutableText.clickCommand(command: String): MutableText {
require(command.startsWith("/"))
return this.styled {
- it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, command))
+ it.withClickEvent(ClickEvent.RunCommand(command))
}
}
@@ -164,4 +197,14 @@ fun Text.transformEachRecursively(function: (Text) -> Text): Text {
fun tr(key: String, default: String): MutableText = error("Compiler plugin did not run.")
fun trResolved(key: String, vararg args: Any): MutableText = Text.stringifiedTranslatable(key, *args)
+fun titleCase(str: String): String {
+ return str
+ .lowercase()
+ .replace("_", " ")
+ .split(" ")
+ .joinToString(" ") { word ->
+ word.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
+ }
+}
+
diff --git a/src/main/kotlin/util/uuid.kt b/src/main/kotlin/util/uuid.kt
index cccfdd2..14aa83d 100644
--- a/src/main/kotlin/util/uuid.kt
+++ b/src/main/kotlin/util/uuid.kt
@@ -3,6 +3,12 @@ package moe.nea.firmament.util
import java.math.BigInteger
import java.util.UUID
+fun parsePotentiallyDashlessUUID(unknownFormattedUUID: String): UUID {
+ if ("-" in unknownFormattedUUID)
+ return UUID.fromString(unknownFormattedUUID)
+ return parseDashlessUUID(unknownFormattedUUID)
+}
+
fun parseDashlessUUID(dashlessUuid: String): UUID {
val most = BigInteger(dashlessUuid.substring(0, 16), 16)
val least = BigInteger(dashlessUuid.substring(16, 32), 16)
diff --git a/src/main/resources/assets/firmament/gui/config/macros/combos.xml b/src/main/resources/assets/firmament/gui/config/macros/combos.xml
new file mode 100644
index 0000000..5141125
--- /dev/null
+++ b/src/main/resources/assets/firmament/gui/config/macros/combos.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig"
+>
+ <Panel background="TRANSPARENT" insets="10">
+ <Column>
+ <ScrollPanel width="380" height="300">
+ <Align horizontal="CENTER">
+ <Array data="@actions">
+ <!-- evenBackground="#8B8B8B" oddBackground="#C6C6C6" -->
+ <Panel background="TRANSPARENT" insets="3">
+ <Panel background="VANILLA" insets="6">
+ <Column>
+ <Row>
+ <Text text="@command" width="280"/>
+ </Row>
+ <Row>
+ <Text text="@formattedCombo" width="250"/>
+ <Align horizontal="RIGHT">
+ <Row>
+ <firm:Button onClick="@edit">
+ <Text text="Edit"/>
+ </firm:Button>
+ <Spacer width="12"/>
+ <firm:Button onClick="@delete">
+ <Text text="Delete"/>
+ </firm:Button>
+ </Row>
+ </Align>
+ </Row>
+ </Column>
+ </Panel>
+
+ </Panel>
+ </Array>
+ </Align>
+ </ScrollPanel>
+ <Align horizontal="RIGHT">
+ <Row>
+ <firm:Button onClick="@discard">
+ <Text text="Discard Changes"/>
+ </firm:Button>
+ <firm:Button onClick="@saveAndClose">
+ <Text text="Save &amp; Close"/>
+ </firm:Button>
+ <firm:Button onClick="@save">
+ <Text text="Save"/>
+ </firm:Button>
+ <firm:Button onClick="@addCommand">
+ <Text text="Add Combo Command"/>
+ </firm:Button>
+ </Row>
+ </Align>
+ </Column>
+ </Panel>
+</Root>
diff --git a/src/main/resources/assets/firmament/gui/config/macros/editor_combo.xml b/src/main/resources/assets/firmament/gui/config/macros/editor_combo.xml
new file mode 100644
index 0000000..50a1d99
--- /dev/null
+++ b/src/main/resources/assets/firmament/gui/config/macros/editor_combo.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig"
+>
+ <Center>
+ <Panel background="VANILLA" insets="10">
+ <Column>
+ <Row>
+ <firm:Button onClick="@back">
+ <Text text="←"/>
+ </firm:Button>
+ <Text text="Editing command macro"/>
+ </Row>
+ <Row>
+ <Text text="Command: /"/>
+ <Align horizontal="RIGHT">
+ <TextField value="@command" width="200"/>
+ </Align>
+ </Row>
+ <Row>
+ <Text text="Key Combo:"/>
+ <Align horizontal="RIGHT">
+ <firm:Button onClick="@addStep">
+ <Text text="+"/>
+ </firm:Button>
+ </Align>
+ </Row>
+ <Array data="@combo">
+ <Row>
+ <firm:Fixed width="160">
+ <Indirect value="@button"/>
+ </firm:Fixed>
+ <Align horizontal="RIGHT">
+ <firm:Button onClick="@delete">
+ <Text text="Delete"/>
+ </firm:Button>
+ </Align>
+ </Row>
+ </Array>
+ </Column>
+ </Panel>
+ </Center>
+</Root>
diff --git a/src/main/resources/assets/firmament/gui/config/macros/editor_wheel.xml b/src/main/resources/assets/firmament/gui/config/macros/editor_wheel.xml
new file mode 100644
index 0000000..e4dc2b4
--- /dev/null
+++ b/src/main/resources/assets/firmament/gui/config/macros/editor_wheel.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig"
+>
+ <Center>
+ <Panel background="VANILLA" insets="10">
+ <Column>
+ <Row>
+ <firm:Button onClick="@back">
+ <Text text="←"/>
+ </firm:Button>
+ <Text text="Editing wheel macro"/>
+ </Row>
+ <Row>
+ <Text text="Key (Hold):"/>
+ <Align horizontal="RIGHT">
+ <firm:Fixed width="160">
+ <Indirect value="@button"/>
+ </firm:Fixed>
+ </Align>
+ </Row>
+ <Row>
+ <Text text="Menu Options:"/>
+ <Align horizontal="RIGHT">
+ <firm:Button onClick="@addOption">
+ <Text text="+"/>
+ </firm:Button>
+ </Align>
+ </Row>
+ <Array data="@editableCommands">
+ <Row>
+ <Text text="/"/>
+ <TextField value="@text" width="160"/>
+ <Align horizontal="RIGHT">
+ <firm:Button onClick="@delete">
+ <Text text="Delete"/>
+ </firm:Button>
+ </Align>
+ </Row>
+ </Array>
+ </Column>
+ </Panel>
+ </Center>
+</Root>
diff --git a/src/main/resources/assets/firmament/gui/config/macros/index.xml b/src/main/resources/assets/firmament/gui/config/macros/index.xml
new file mode 100644
index 0000000..f6a1545
--- /dev/null
+++ b/src/main/resources/assets/firmament/gui/config/macros/index.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<Root xmlns="http://notenoughupdates.org/moulconfig"
+>
+ <Center>
+ <Row>
+ <Tabs>
+ <Tab>
+ <Tab.Header>
+ <Text text="Combo Macros"/>
+ </Tab.Header>
+ <Tab.Body>
+ <Fragment value="firmament:gui/config/macros/combos.xml" bind="@combos"/>
+ </Tab.Body>
+ </Tab>
+ <Tab>
+ <Tab.Header>
+ <Text text="Macro Wheel"/>
+ </Tab.Header>
+ <Tab.Body>
+ <Fragment value="firmament:gui/config/macros/wheel.xml" bind="@wheels"/>
+ </Tab.Body>
+ </Tab>
+ </Tabs>
+ <Meta beforeClose="@beforeClose"/>
+ </Row>
+ </Center>
+</Root>
diff --git a/src/main/resources/assets/firmament/gui/config/macros/wheel.xml b/src/main/resources/assets/firmament/gui/config/macros/wheel.xml
new file mode 100644
index 0000000..19922fe
--- /dev/null
+++ b/src/main/resources/assets/firmament/gui/config/macros/wheel.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig"
+>
+ <Panel background="TRANSPARENT" insets="10">
+ <Column>
+ <ScrollPanel width="380" height="300">
+ <Align horizontal="CENTER">
+ <Array data="@wheels">
+ <Panel background="TRANSPARENT" insets="3">
+ <Panel background="VANILLA" insets="6">
+ <Column>
+ <Row>
+ <Text text="@keyCombo" width="250"/>
+ <Align horizontal="RIGHT">
+ <Row>
+ <firm:Button onClick="@edit">
+ <Text text="Edit"/>
+ </firm:Button>
+ <Spacer width="12"/>
+ <firm:Button onClick="@delete">
+ <Text text="Delete"/>
+ </firm:Button>
+ </Row>
+ </Align>
+ </Row>
+ <Array data="@commands">
+ <Text text="@text" width="280"/>
+ </Array>
+ </Column>
+ </Panel>
+
+ </Panel>
+ </Array>
+ </Align>
+ </ScrollPanel>
+ <Align horizontal="RIGHT">
+ <Row>
+ <firm:Button onClick="@discard">
+ <Text text="Discard Changes"/>
+ </firm:Button>
+ <firm:Button onClick="@saveAndClose">
+ <Text text="Save &amp; Close"/>
+ </firm:Button>
+ <firm:Button onClick="@save">
+ <Text text="Save"/>
+ </firm:Button>
+ <firm:Button onClick="@addWheel">
+ <Text text="Add Wheel"/>
+ </firm:Button>
+ </Row>
+ </Align>
+ </Column>
+ </Panel>
+</Root>
diff --git a/src/main/resources/assets/firmament/gui/license_viewer/index.xml b/src/main/resources/assets/firmament/gui/license_viewer/index.xml
new file mode 100644
index 0000000..c23153d
--- /dev/null
+++ b/src/main/resources/assets/firmament/gui/license_viewer/index.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<Root xmlns="http://notenoughupdates.org/moulconfig"
+ xmlns:firm="http://firmament.nea.moe/moulconfig"
+>
+ <Center>
+ <Panel background="VANILLA">
+ <Column>
+ <Center>
+ <Scale scale="2">
+ <Text text="Firmament Licenses"/>
+ </Scale>
+ </Center>
+ <!-- <firm:Line/>-->
+ <ScrollPanel width="306" height="250">
+ <Panel insets="3" background="TRANSPARENT">
+ <Array data="@softwares">
+ <Center>
+ <firm:Fixed width="300">
+ <Panel background="VANILLA" insets="8">
+ <Column>
+ <Scale scale="1.2">
+ <Text text="@projectName"/>
+ </Scale>
+ <When condition="@hasWebPresence">
+ <Row>
+ <firm:Button onClick="@open">
+ <Text text="Navigate to WebSite"/>
+ </firm:Button>
+ </Row>
+ <Spacer/>
+ </When>
+ <Text text="@projectDescription" width="280"/>
+ <Array data="@developers">
+ <Row>
+ <Text text="by "/>
+ <Text text="@name"/>
+ </Row>
+ </Array>
+ <Array data="@licenses">
+ <When condition="@hasUrl">
+ <firm:Button onClick="@open">
+ <Center>
+ <Row>
+ <Text text="License: "/>
+ <Text text="@name"/>
+ </Row>
+ </Center>
+ </firm:Button>
+ <Row>
+ <Text text="License: "/>
+ <Text text="@name"/>
+ </Row>
+ </When>
+ </Array>
+ </Column>
+ </Panel>
+ </firm:Fixed>
+ </Center>
+ </Array>
+ </Panel>
+ </ScrollPanel>
+ </Column>
+ </Panel>
+ </Center>
+</Root>
diff --git a/src/main/resources/assets/firmament/logo.png b/src/main/resources/assets/firmament/logo.png
index e00a2fa..e3f063a 100644
--- a/src/main/resources/assets/firmament/logo.png
+++ b/src/main/resources/assets/firmament/logo.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/shaders/cape/parallax.fsh b/src/main/resources/assets/firmament/shaders/cape/parallax.fsh
new file mode 100644
index 0000000..bc9a440
--- /dev/null
+++ b/src/main/resources/assets/firmament/shaders/cape/parallax.fsh
@@ -0,0 +1,53 @@
+#version 150
+
+#moj_import <minecraft:fog.glsl>
+#define M_PI 3.1415926535897932384626433832795
+#define M_TAU (2.0 * M_PI)
+uniform sampler2D Sampler0;
+uniform sampler2D Sampler1;
+uniform sampler2D Sampler3;
+
+uniform vec4 ColorModulator;
+uniform float FogStart;
+uniform float FogEnd;
+uniform vec4 FogColor;
+uniform float Animation;
+
+in float vertexDistance;
+in vec4 vertexColor;
+in vec4 lightMapColor;
+in vec4 overlayColor;
+in vec2 texCoord0;
+
+out vec4 fragColor;
+
+float highlightDistance(vec2 coord, vec2 direction, float time) {
+ vec2 dir = normalize(direction);
+ float projection = dot(coord, dir);
+ float animationTime = sin(projection + time * 13 * M_TAU);
+ if (animationTime < 0.997) {
+ return 0.0;
+ }
+ return animationTime;
+}
+
+void main() {
+ vec4 color = texture(Sampler0, texCoord0);
+ if (color.g > 0.99) {
+ // TODO: maybe this speed in each direction should be a uniform
+ color = texture(Sampler1, texCoord0 + Animation * vec2(3.0, -2.0));
+ }
+
+ vec4 highlightColor = texture(Sampler3, texCoord0);
+ if (highlightColor.a > 0.5) {
+ color = highlightColor;
+ float animationHighlight = highlightDistance(texCoord0, vec2(-12.0, 2.0), Animation);
+ color.rgb += (animationHighlight);
+ }
+ #ifdef ALPHA_CUTOUT
+ if (color.a < ALPHA_CUTOUT) {
+ discard;
+ }
+ #endif
+ fragColor = linear_fog(color, vertexDistance, FogStart, FogEnd, FogColor);
+}
diff --git a/src/main/resources/assets/firmament/shaders/circle_discard_color.fsh b/src/main/resources/assets/firmament/shaders/circle_discard_color.fsh
new file mode 100644
index 0000000..ae46059
--- /dev/null
+++ b/src/main/resources/assets/firmament/shaders/circle_discard_color.fsh
@@ -0,0 +1,22 @@
+#version 150
+
+in vec4 vertexColor;
+in vec2 texCoord0;
+
+uniform vec4 ColorModulator;
+uniform float InnerCutoutRadius;
+
+out vec4 fragColor;
+
+void main() {
+ vec4 color = vertexColor;
+ if (color.a == 0.0) {
+ discard;
+ }
+ float d = length(texCoord0 - vec2(0.5));
+ if (d > 0.5 || d < InnerCutoutRadius)
+ {
+ discard;
+ }
+ fragColor = color * ColorModulator;
+}
diff --git a/src/main/resources/assets/firmament/textures/cape/REUSE.toml b/src/main/resources/assets/firmament/textures/cape/REUSE.toml
new file mode 100644
index 0000000..ba721f7
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/cape/REUSE.toml
@@ -0,0 +1,19 @@
+#SPDX-FileCopyrightText: 2025 Linnea Gräf <nea@nea.moe>
+#
+#SPDX-License-Identifier: CC0-1.0
+version = 1
+
+[[annotations]]
+path = ["firmament_star.png", "parallax_background.png", "parallax_template.png"]
+SPDX-License-Identifier = "CC-BY-4.0"
+SPDX-FileCopyrightText = ["ic22487", "Linnea Gräf"]
+
+[[annotations]]
+path = ["firm_static.png"]
+SPDX-License-Identifier = "CC-BY-4.0"
+SPDX-FileCopyrightText = ["ic22487", "kathund"]
+
+[[annotations]]
+path = ["fsr_static.png"]
+SPDX-License-Identifier = "CC-BY-4.0"
+SPDX-FileCopyrightText = ["Tendan"]
diff --git a/src/main/resources/assets/firmament/textures/cape/firm_static.png b/src/main/resources/assets/firmament/textures/cape/firm_static.png
new file mode 100644
index 0000000..b01511c
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/cape/firm_static.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/textures/cape/firmament_star.png b/src/main/resources/assets/firmament/textures/cape/firmament_star.png
new file mode 100644
index 0000000..520d309
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/cape/firmament_star.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/textures/cape/fsr_static.png b/src/main/resources/assets/firmament/textures/cape/fsr_static.png
new file mode 100644
index 0000000..de9cf35
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/cape/fsr_static.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/textures/cape/parallax_background.png b/src/main/resources/assets/firmament/textures/cape/parallax_background.png
new file mode 100644
index 0000000..05ef0fa
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/cape/parallax_background.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/textures/cape/parallax_template.png b/src/main/resources/assets/firmament/textures/cape/parallax_template.png
new file mode 100644
index 0000000..7084c12
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/cape/parallax_template.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png
index 97dd0ea..c897840 100644
--- a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png
Binary files differ
diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json
index 02c11ee..115778f 100644
--- a/src/main/resources/fabric.mod.json
+++ b/src/main/resources/fabric.mod.json
@@ -51,7 +51,7 @@
"firmament.mixins.json"
],
"depends": {
- "fabric": ">=${fabric_api_version}",
+ "fabric-api": ">=${fabric_api_version}",
"fabric-language-kotlin": ">=${fabric_kotlin_version}",
"minecraft": ">=${minecraft_version}"
},
diff --git a/src/main/resources/firmament.accesswidener b/src/main/resources/firmament.accesswidener
index fd79cb5..71f63ac 100644
--- a/src/main/resources/firmament.accesswidener
+++ b/src/main/resources/firmament.accesswidener
@@ -2,16 +2,17 @@ accessWidener v2 named
accessible class net/minecraft/client/render/RenderLayer$MultiPhase
accessible class net/minecraft/client/render/RenderLayer$MultiPhaseParameters
accessible class net/minecraft/client/font/TextRenderer$Drawer
+
accessible field net/minecraft/client/gui/hud/InGameHud SCOREBOARD_ENTRY_COMPARATOR Ljava/util/Comparator;
+
accessible field net/minecraft/client/network/ClientPlayNetworkHandler combinedDynamicRegistries Lnet/minecraft/registry/DynamicRegistryManager$Immutable;
accessible method net/minecraft/registry/RegistryOps <init> (Lcom/mojang/serialization/DynamicOps;Lnet/minecraft/registry/RegistryOps$RegistryInfoGetter;)V
accessible class net/minecraft/registry/RegistryOps$CachedRegistryInfoGetter
+accessible class net/minecraft/client/render/model/ModelBaker$BakerImpl
+accessible method net/minecraft/client/render/model/ModelBaker$BakerImpl <init> (Lnet/minecraft/client/render/model/ModelBaker;Lnet/minecraft/client/render/model/ErrorCollectingSpriteGetter;)V
accessible field net/minecraft/entity/mob/CreeperEntity CHARGED Lnet/minecraft/entity/data/TrackedData;
accessible method net/minecraft/entity/decoration/ArmorStandEntity setSmall (Z)V
-accessible field net/minecraft/entity/passive/AbstractHorseEntity items Lnet/minecraft/inventory/SimpleInventory;
-accessible field net/minecraft/entity/passive/AbstractHorseEntity SADDLED_FLAG I
-accessible field net/minecraft/entity/passive/AbstractHorseEntity HORSE_FLAGS Lnet/minecraft/entity/data/TrackedData;
accessible method net/minecraft/resource/NamespaceResourceManager loadMetadata (Lnet/minecraft/resource/InputSupplier;)Lnet/minecraft/resource/metadata/ResourceMetadata;
accessible method net/minecraft/client/gui/DrawContext drawTexturedQuad (Ljava/util/function/Function;Lnet/minecraft/util/Identifier;IIIIFFFFI)V
@@ -26,3 +27,5 @@ accessible method net/minecraft/client/render/RenderPhase$Texture getId ()Ljava/
accessible field net/minecraft/client/render/RenderLayer$MultiPhase phases Lnet/minecraft/client/render/RenderLayer$MultiPhaseParameters;
accessible field net/minecraft/client/render/RenderLayer$MultiPhaseParameters texture Lnet/minecraft/client/render/RenderPhase$TextureBase;
accessible field net/minecraft/client/network/ClientPlayerInteractionManager currentBreakingPos Lnet/minecraft/util/math/BlockPos;
+
+mutable field net/minecraft/client/render/entity/state/LivingEntityRenderState headItemRenderState Lnet/minecraft/client/render/item/ItemRenderState;
diff --git a/src/main/resources/legacy_data/enchantments.json b/src/main/resources/legacy_data/enchantments.json
new file mode 100644
index 0000000..8eeaa6e
--- /dev/null
+++ b/src/main/resources/legacy_data/enchantments.json
@@ -0,0 +1,560 @@
+[
+ {
+ "id": 0,
+ "name": "protection",
+ "displayName": "Protection",
+ "maxLevel": 4,
+ "minCost": {
+ "a": 11,
+ "b": -10
+ },
+ "maxCost": {
+ "a": 11,
+ "b": 1
+ },
+ "exclude": [
+ "blast_protection",
+ "fire_protection",
+ "projectile_protection"
+ ],
+ "category": "armor",
+ "weight": 10,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 1,
+ "name": "fire_protection",
+ "displayName": "Fire Protection",
+ "maxLevel": 4,
+ "minCost": {
+ "a": 8,
+ "b": 2
+ },
+ "maxCost": {
+ "a": 8,
+ "b": 10
+ },
+ "exclude": [
+ "blast_protection",
+ "protection",
+ "projectile_protection"
+ ],
+ "category": "armor",
+ "weight": 5,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 2,
+ "name": "feather_falling",
+ "displayName": "Feather Falling",
+ "maxLevel": 4,
+ "minCost": {
+ "a": 6,
+ "b": -1
+ },
+ "maxCost": {
+ "a": 6,
+ "b": 5
+ },
+ "exclude": [],
+ "category": "armor_feet",
+ "weight": 5,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 3,
+ "name": "blast_protection",
+ "displayName": "Blast Protection",
+ "maxLevel": 4,
+ "minCost": {
+ "a": 8,
+ "b": -3
+ },
+ "maxCost": {
+ "a": 8,
+ "b": 5
+ },
+ "exclude": [
+ "fire_protection",
+ "protection",
+ "projectile_protection"
+ ],
+ "category": "armor",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 4,
+ "name": "projectile_protection",
+ "displayName": "Projectile Protection",
+ "maxLevel": 4,
+ "minCost": {
+ "a": 6,
+ "b": -3
+ },
+ "maxCost": {
+ "a": 6,
+ "b": 3
+ },
+ "exclude": [
+ "protection",
+ "blast_protection",
+ "fire_protection"
+ ],
+ "category": "armor",
+ "weight": 5,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 5,
+ "name": "respiration",
+ "displayName": "Respiration",
+ "maxLevel": 3,
+ "minCost": {
+ "a": 10,
+ "b": 0
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 30
+ },
+ "exclude": [],
+ "category": "armor_head",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 6,
+ "name": "aqua_affinity",
+ "displayName": "Aqua Affinity",
+ "maxLevel": 1,
+ "minCost": {
+ "a": 0,
+ "b": 1
+ },
+ "maxCost": {
+ "a": 0,
+ "b": 41
+ },
+ "exclude": [],
+ "category": "armor_head",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 7,
+ "name": "thorns",
+ "displayName": "Thorns",
+ "maxLevel": 3,
+ "minCost": {
+ "a": 20,
+ "b": -10
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [],
+ "category": "armor_chest",
+ "weight": 1,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 8,
+ "name": "depth_strider",
+ "displayName": "Depth Strider",
+ "maxLevel": 3,
+ "minCost": {
+ "a": 10,
+ "b": 0
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 15
+ },
+ "exclude": [
+ "frost_walker"
+ ],
+ "category": "armor_feet",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 16,
+ "name": "sharpness",
+ "displayName": "Sharpness",
+ "maxLevel": 5,
+ "minCost": {
+ "a": 11,
+ "b": -10
+ },
+ "maxCost": {
+ "a": 11,
+ "b": 10
+ },
+ "exclude": [
+ "smite",
+ "bane_of_arthropods"
+ ],
+ "category": "weapon",
+ "weight": 10,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 17,
+ "name": "smite",
+ "displayName": "Smite",
+ "maxLevel": 5,
+ "minCost": {
+ "a": 8,
+ "b": -3
+ },
+ "maxCost": {
+ "a": 8,
+ "b": 17
+ },
+ "exclude": [
+ "sharpness",
+ "bane_of_arthropods"
+ ],
+ "category": "weapon",
+ "weight": 5,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 18,
+ "name": "bane_of_arthropods",
+ "displayName": "Bane of Arthropods",
+ "maxLevel": 5,
+ "minCost": {
+ "a": 8,
+ "b": -3
+ },
+ "maxCost": {
+ "a": 8,
+ "b": 17
+ },
+ "exclude": [
+ "smite",
+ "sharpness"
+ ],
+ "category": "weapon",
+ "weight": 5,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 19,
+ "name": "knockback",
+ "displayName": "Knockback",
+ "maxLevel": 2,
+ "minCost": {
+ "a": 20,
+ "b": -15
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [],
+ "category": "weapon",
+ "weight": 5,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 20,
+ "name": "fire_aspect",
+ "displayName": "Fire Aspect",
+ "maxLevel": 2,
+ "minCost": {
+ "a": 20,
+ "b": -10
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [],
+ "category": "weapon",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 21,
+ "name": "looting",
+ "displayName": "Looting",
+ "maxLevel": 3,
+ "minCost": {
+ "a": 9,
+ "b": 6
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [],
+ "category": "weapon",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 32,
+ "name": "efficiency",
+ "displayName": "Efficiency",
+ "maxLevel": 5,
+ "minCost": {
+ "a": 10,
+ "b": -9
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [],
+ "category": "digger",
+ "weight": 10,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 33,
+ "name": "silk_touch",
+ "displayName": "Silk Touch",
+ "maxLevel": 1,
+ "minCost": {
+ "a": 0,
+ "b": 15
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [
+ "fortune"
+ ],
+ "category": "digger",
+ "weight": 1,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 34,
+ "name": "unbreaking",
+ "displayName": "Unbreaking",
+ "maxLevel": 3,
+ "minCost": {
+ "a": 8,
+ "b": -3
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [],
+ "category": "breakable",
+ "weight": 5,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 35,
+ "name": "fortune",
+ "displayName": "Fortune",
+ "maxLevel": 3,
+ "minCost": {
+ "a": 9,
+ "b": 6
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [
+ "silk_touch"
+ ],
+ "category": "digger",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 48,
+ "name": "power",
+ "displayName": "Power",
+ "maxLevel": 5,
+ "minCost": {
+ "a": 10,
+ "b": -9
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 6
+ },
+ "exclude": [],
+ "category": "bow",
+ "weight": 10,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 49,
+ "name": "punch",
+ "displayName": "Punch",
+ "maxLevel": 2,
+ "minCost": {
+ "a": 20,
+ "b": -8
+ },
+ "maxCost": {
+ "a": 20,
+ "b": 17
+ },
+ "exclude": [],
+ "category": "bow",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 50,
+ "name": "flame",
+ "displayName": "Flame",
+ "maxLevel": 1,
+ "minCost": {
+ "a": 0,
+ "b": 20
+ },
+ "maxCost": {
+ "a": 0,
+ "b": 50
+ },
+ "exclude": [],
+ "category": "bow",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 51,
+ "name": "infinity",
+ "displayName": "Infinity",
+ "maxLevel": 1,
+ "minCost": {
+ "a": 0,
+ "b": 20
+ },
+ "maxCost": {
+ "a": 0,
+ "b": 50
+ },
+ "exclude": [
+ "mending"
+ ],
+ "category": "bow",
+ "weight": 1,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 61,
+ "name": "luck_of_the_sea",
+ "displayName": "Luck of the Sea",
+ "maxLevel": 3,
+ "minCost": {
+ "a": 9,
+ "b": 6
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [],
+ "category": "fishing_rod",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 62,
+ "name": "lure",
+ "displayName": "Lure",
+ "maxLevel": 3,
+ "minCost": {
+ "a": 9,
+ "b": 6
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [],
+ "category": "fishing_rod",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ }
+]
diff --git a/src/main/resources/legacy_data/items.json b/src/main/resources/legacy_data/items.json
new file mode 100644
index 0000000..a32702c
--- /dev/null
+++ b/src/main/resources/legacy_data/items.json
@@ -0,0 +1,3733 @@
+[
+ {
+ "id": 1,
+ "displayName": "Stone",
+ "name": "stone",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Stone"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Granite"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Polished Granite"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Diorite"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Polished Diorite"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Andesite"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Polished Andesite"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "displayName": "Grass Block",
+ "name": "grass",
+ "stackSize": 64
+ },
+ {
+ "id": 3,
+ "displayName": "Dirt",
+ "name": "dirt",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Dirt"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Coarse Dirt"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Podzol"
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "displayName": "Cobblestone",
+ "name": "cobblestone",
+ "stackSize": 64
+ },
+ {
+ "id": 5,
+ "displayName": "Wooden Planks",
+ "name": "planks",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Oak Wood Planks"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Spruce Wood Planks"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Birch Wood Planks"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Jungle Wood Planks"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Acacia Wood Planks"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Dark Oak Wood Planks"
+ }
+ ]
+ },
+ {
+ "id": 6,
+ "displayName": "Sapling",
+ "name": "sapling",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Oak Sapling"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Spruce Sapling"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Birch Sapling"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Jungle Sapling"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Acacia Sapling"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Dark Oak Sapling"
+ }
+ ]
+ },
+ {
+ "id": 7,
+ "displayName": "Bedrock",
+ "name": "bedrock",
+ "stackSize": 64
+ },
+ {
+ "id": 12,
+ "displayName": "Sand",
+ "name": "sand",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Sand"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Red Sand"
+ }
+ ]
+ },
+ {
+ "id": 13,
+ "displayName": "Gravel",
+ "name": "gravel",
+ "stackSize": 64
+ },
+ {
+ "id": 14,
+ "displayName": "Gold Ore",
+ "name": "gold_ore",
+ "stackSize": 64
+ },
+ {
+ "id": 15,
+ "displayName": "Iron Ore",
+ "name": "iron_ore",
+ "stackSize": 64
+ },
+ {
+ "id": 16,
+ "displayName": "Coal Ore",
+ "name": "coal_ore",
+ "stackSize": 64
+ },
+ {
+ "id": 17,
+ "displayName": "Wood",
+ "name": "log",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Oak Wood"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Spruce Wood"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Birch Wood"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Jungle Wood"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Acacia Wood"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Dark Oak Wood"
+ }
+ ]
+ },
+ {
+ "id": 18,
+ "displayName": "Leaves",
+ "name": "leaves",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Oak Leaves"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Spruce Leaves"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Birch Leaves"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Jungle Leaves"
+ }
+ ]
+ },
+ {
+ "id": 19,
+ "displayName": "Sponge",
+ "name": "sponge",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Sponge"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Wet Sponge"
+ }
+ ]
+ },
+ {
+ "id": 20,
+ "displayName": "Glass",
+ "name": "glass",
+ "stackSize": 64
+ },
+ {
+ "id": 21,
+ "displayName": "Lapis Lazuli Ore",
+ "name": "lapis_ore",
+ "stackSize": 64
+ },
+ {
+ "id": 22,
+ "displayName": "Lapis Lazuli Block",
+ "name": "lapis_block",
+ "stackSize": 64
+ },
+ {
+ "id": 23,
+ "displayName": "Dispenser",
+ "name": "dispenser",
+ "stackSize": 64
+ },
+ {
+ "id": 24,
+ "displayName": "Sandstone",
+ "name": "sandstone",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Sandstone"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Chiseled Sandstone"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Smooth Sandstone"
+ }
+ ]
+ },
+ {
+ "id": 25,
+ "displayName": "Note Block",
+ "name": "noteblock",
+ "stackSize": 64
+ },
+ {
+ "id": 27,
+ "displayName": "Powered Rail",
+ "name": "golden_rail",
+ "stackSize": 64
+ },
+ {
+ "id": 28,
+ "displayName": "Detector Rail",
+ "name": "detector_rail",
+ "stackSize": 64
+ },
+ {
+ "id": 29,
+ "displayName": "Sticky Piston",
+ "name": "sticky_piston",
+ "stackSize": 64
+ },
+ {
+ "id": 30,
+ "displayName": "Cobweb",
+ "name": "web",
+ "stackSize": 64
+ },
+ {
+ "id": 31,
+ "displayName": "Grass",
+ "name": "tallgrass",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Shrub"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Tall Grass"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Fern"
+ }
+ ]
+ },
+ {
+ "id": 32,
+ "displayName": "Dead Bush",
+ "name": "deadbush",
+ "stackSize": 64
+ },
+ {
+ "id": 33,
+ "displayName": "Piston",
+ "name": "piston",
+ "stackSize": 64
+ },
+ {
+ "id": 35,
+ "displayName": "Wool",
+ "name": "wool",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "White Wool"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Orange Wool"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Magenta Wool"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Light blue Wool"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Yellow Wool"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Lime Wool"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Pink Wool"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Gray Wool"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Light gray Wool"
+ },
+ {
+ "metadata": 9,
+ "displayName": "Cyan Wool"
+ },
+ {
+ "metadata": 10,
+ "displayName": "Purple Wool"
+ },
+ {
+ "metadata": 11,
+ "displayName": "Blue Wool"
+ },
+ {
+ "metadata": 12,
+ "displayName": "Brown Wool"
+ },
+ {
+ "metadata": 13,
+ "displayName": "Green Wool"
+ },
+ {
+ "metadata": 14,
+ "displayName": "Red Wool"
+ },
+ {
+ "metadata": 15,
+ "displayName": "Black Wool"
+ }
+ ]
+ },
+ {
+ "id": 37,
+ "displayName": "Dandelion",
+ "name": "yellow_flower",
+ "stackSize": 64
+ },
+ {
+ "id": 38,
+ "displayName": "Poppy",
+ "name": "red_flower",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Poppy"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Blue Orchid"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Allium"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Azure Bluet"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Red Tulip"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Orange Tulip"
+ },
+ {
+ "metadata": 6,
+ "displayName": "White Tulip"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Pink Tulip"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Oxeye Daisy"
+ }
+ ]
+ },
+ {
+ "id": 39,
+ "displayName": "Brown Mushroom",
+ "name": "brown_mushroom",
+ "stackSize": 64
+ },
+ {
+ "id": 40,
+ "displayName": "Red Mushroom",
+ "name": "red_mushroom",
+ "stackSize": 64
+ },
+ {
+ "id": 41,
+ "displayName": "Block of Gold",
+ "name": "gold_block",
+ "stackSize": 64
+ },
+ {
+ "id": 42,
+ "displayName": "Block of Iron",
+ "name": "iron_block",
+ "stackSize": 64
+ },
+ {
+ "id": 44,
+ "displayName": "Stone Slab",
+ "name": "stone_slab",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Stone Slab"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Sandstone Slab"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Wooden Slab"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Cobblestone Slab"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Bricks Slab"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Stone Bricks Slab"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Nether Brick Slab"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Quartz Slab"
+ }
+ ]
+ },
+ {
+ "id": 45,
+ "displayName": "Brick",
+ "name": "brick_block",
+ "stackSize": 64
+ },
+ {
+ "id": 46,
+ "displayName": "TNT",
+ "name": "tnt",
+ "stackSize": 64
+ },
+ {
+ "id": 47,
+ "displayName": "Bookshelf",
+ "name": "bookshelf",
+ "stackSize": 64
+ },
+ {
+ "id": 48,
+ "displayName": "Moss Stone",
+ "name": "mossy_cobblestone",
+ "stackSize": 64
+ },
+ {
+ "id": 49,
+ "displayName": "Obsidian",
+ "name": "obsidian",
+ "stackSize": 64
+ },
+ {
+ "id": 50,
+ "displayName": "Torch",
+ "name": "torch",
+ "stackSize": 64
+ },
+ {
+ "id": 52,
+ "displayName": "Monster Spawner",
+ "name": "mob_spawner",
+ "stackSize": 64
+ },
+ {
+ "id": 53,
+ "displayName": "Oak Wood Stairs",
+ "name": "oak_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 54,
+ "displayName": "Chest",
+ "name": "chest",
+ "stackSize": 64
+ },
+ {
+ "id": 56,
+ "displayName": "Diamond Ore",
+ "name": "diamond_ore",
+ "stackSize": 64
+ },
+ {
+ "id": 57,
+ "displayName": "Block of Diamond",
+ "name": "diamond_block",
+ "stackSize": 64
+ },
+ {
+ "id": 58,
+ "displayName": "Crafting Table",
+ "name": "crafting_table",
+ "stackSize": 64
+ },
+ {
+ "id": 60,
+ "displayName": "Farmland",
+ "name": "farmland",
+ "stackSize": 64
+ },
+ {
+ "id": 61,
+ "displayName": "Furnace",
+ "name": "furnace",
+ "stackSize": 64
+ },
+ {
+ "id": 65,
+ "displayName": "Ladder",
+ "name": "ladder",
+ "stackSize": 64
+ },
+ {
+ "id": 66,
+ "displayName": "Rail",
+ "name": "rail",
+ "stackSize": 64
+ },
+ {
+ "id": 67,
+ "displayName": "Cobblestone Stairs",
+ "name": "stone_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 69,
+ "displayName": "Lever",
+ "name": "lever",
+ "stackSize": 64
+ },
+ {
+ "id": 70,
+ "displayName": "Stone Pressure Plate",
+ "name": "stone_pressure_plate",
+ "stackSize": 64
+ },
+ {
+ "id": 72,
+ "displayName": "Wooden Pressure Plate",
+ "name": "wooden_pressure_plate",
+ "stackSize": 64
+ },
+ {
+ "id": 73,
+ "displayName": "Redstone Ore",
+ "name": "redstone_ore",
+ "stackSize": 64
+ },
+ {
+ "id": 76,
+ "displayName": "Redstone Torch",
+ "name": "redstone_torch",
+ "stackSize": 64
+ },
+ {
+ "id": 77,
+ "displayName": "Stone Button",
+ "name": "stone_button",
+ "stackSize": 64
+ },
+ {
+ "id": 78,
+ "displayName": "Snow",
+ "name": "snow_layer",
+ "stackSize": 64
+ },
+ {
+ "id": 79,
+ "displayName": "Ice",
+ "name": "ice",
+ "stackSize": 64
+ },
+ {
+ "id": 80,
+ "displayName": "Snow",
+ "name": "snow",
+ "stackSize": 64
+ },
+ {
+ "id": 81,
+ "displayName": "Cactus",
+ "name": "cactus",
+ "stackSize": 64
+ },
+ {
+ "id": 82,
+ "displayName": "Clay",
+ "name": "clay",
+ "stackSize": 64
+ },
+ {
+ "id": 84,
+ "displayName": "Jukebox",
+ "name": "jukebox",
+ "stackSize": 64
+ },
+ {
+ "id": 85,
+ "displayName": "Oak Fence",
+ "name": "fence",
+ "stackSize": 64
+ },
+ {
+ "id": 86,
+ "displayName": "Pumpkin",
+ "name": "pumpkin",
+ "stackSize": 64
+ },
+ {
+ "id": 87,
+ "displayName": "Netherrack",
+ "name": "netherrack",
+ "stackSize": 64
+ },
+ {
+ "id": 88,
+ "displayName": "Soul Sand",
+ "name": "soul_sand",
+ "stackSize": 64
+ },
+ {
+ "id": 89,
+ "displayName": "Glowstone",
+ "name": "glowstone",
+ "stackSize": 64
+ },
+ {
+ "id": 91,
+ "displayName": "Jack o'Lantern",
+ "name": "lit_pumpkin",
+ "stackSize": 64
+ },
+ {
+ "id": 95,
+ "displayName": "Stained Glass",
+ "name": "stained_glass",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "White Stained Glass"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Orange Stained Glass"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Magenta Stained Glass"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Light Blue Stained Glass"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Yellow Stained Glass"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Lime Stained Glass"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Pink Stained Glass"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Gray Stained Glass"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Light Gray Stained Glass"
+ },
+ {
+ "metadata": 9,
+ "displayName": "Cyan Stained Glass"
+ },
+ {
+ "metadata": 10,
+ "displayName": "Purple Stained Glass"
+ },
+ {
+ "metadata": 11,
+ "displayName": "Blue Stained Glass"
+ },
+ {
+ "metadata": 12,
+ "displayName": "Brown Stained Glass"
+ },
+ {
+ "metadata": 13,
+ "displayName": "Green Stained Glass"
+ },
+ {
+ "metadata": 14,
+ "displayName": "Red Stained Glass"
+ },
+ {
+ "metadata": 15,
+ "displayName": "Black Stained Glass"
+ }
+ ]
+ },
+ {
+ "id": 96,
+ "displayName": "Wooden Trapdoor",
+ "name": "trapdoor",
+ "stackSize": 64
+ },
+ {
+ "id": 97,
+ "displayName": "Monster Egg",
+ "name": "monster_egg",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Stone Monster Egg"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Cobblestone Monster Egg"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Stone Brick Monster Egg"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Mossy Stone Brick Monster Egg"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Cracked Stone Brick Monster Egg"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Chiseled Stone Brick Monster Egg"
+ }
+ ]
+ },
+ {
+ "id": 98,
+ "displayName": "Stone Bricks",
+ "name": "stonebrick",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Stone Bricks"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Mossy Stone Bricks"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Cracked Stone Bricks"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Chiseled Stone Bricks"
+ }
+ ]
+ },
+ {
+ "id": 99,
+ "displayName": "Brown Mushroom Block",
+ "name": "brown_mushroom_block",
+ "stackSize": 64
+ },
+ {
+ "id": 100,
+ "displayName": "Red Mushroom Block",
+ "name": "red_mushroom_block",
+ "stackSize": 64
+ },
+ {
+ "id": 101,
+ "displayName": "Iron Bars",
+ "name": "iron_bars",
+ "stackSize": 64
+ },
+ {
+ "id": 102,
+ "displayName": "Glass Pane",
+ "name": "glass_pane",
+ "stackSize": 64
+ },
+ {
+ "id": 103,
+ "displayName": "Melon",
+ "name": "melon_block",
+ "stackSize": 64
+ },
+ {
+ "id": 106,
+ "displayName": "Vines",
+ "name": "vine",
+ "stackSize": 64
+ },
+ {
+ "id": 107,
+ "displayName": "Oak Fence Gate",
+ "name": "fence_gate",
+ "stackSize": 64
+ },
+ {
+ "id": 108,
+ "displayName": "Brick Stairs",
+ "name": "brick_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 109,
+ "displayName": "Stone Brick Stairs",
+ "name": "stone_brick_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 110,
+ "displayName": "Mycelium",
+ "name": "mycelium",
+ "stackSize": 64
+ },
+ {
+ "id": 111,
+ "displayName": "Lily Pad",
+ "name": "waterlily",
+ "stackSize": 64
+ },
+ {
+ "id": 112,
+ "displayName": "Nether Brick",
+ "name": "nether_brick",
+ "stackSize": 64
+ },
+ {
+ "id": 113,
+ "displayName": "Nether Brick Fence",
+ "name": "nether_brick_fence",
+ "stackSize": 64
+ },
+ {
+ "id": 114,
+ "displayName": "Nether Brick Stairs",
+ "name": "nether_brick_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 116,
+ "displayName": "Enchantment Table",
+ "name": "enchanting_table",
+ "stackSize": 64
+ },
+ {
+ "id": 120,
+ "displayName": "End Portal Frame",
+ "name": "end_portal_frame",
+ "stackSize": 64
+ },
+ {
+ "id": 121,
+ "displayName": "End Stone",
+ "name": "end_stone",
+ "stackSize": 64
+ },
+ {
+ "id": 122,
+ "displayName": "Dragon Egg",
+ "name": "dragon_egg",
+ "stackSize": 64
+ },
+ {
+ "id": 123,
+ "displayName": "Redstone Lamp",
+ "name": "redstone_lamp",
+ "stackSize": 64
+ },
+ {
+ "id": 126,
+ "displayName": "Wood Slab",
+ "name": "wooden_slab",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Oak Wood Slab"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Spruce Wood Slab"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Birch Wood Slab"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Jungle Wood Slab"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Acacia Wood Slab"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Dark Oak Wood Slab"
+ }
+ ]
+ },
+ {
+ "id": 128,
+ "displayName": "Sandstone Stairs",
+ "name": "sandstone_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 129,
+ "displayName": "Emerald Ore",
+ "name": "emerald_ore",
+ "stackSize": 64
+ },
+ {
+ "id": 130,
+ "displayName": "Ender Chest",
+ "name": "ender_chest",
+ "stackSize": 64
+ },
+ {
+ "id": 131,
+ "displayName": "Tripwire Hook",
+ "name": "tripwire_hook",
+ "stackSize": 64
+ },
+ {
+ "id": 133,
+ "displayName": "Block of Emerald",
+ "name": "emerald_block",
+ "stackSize": 64
+ },
+ {
+ "id": 134,
+ "displayName": "Spruce Wood Stairs",
+ "name": "spruce_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 135,
+ "displayName": "Birch Wood Stairs",
+ "name": "birch_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 136,
+ "displayName": "Jungle Wood Stairs",
+ "name": "jungle_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 137,
+ "displayName": "Command Block",
+ "name": "command_block",
+ "stackSize": 64
+ },
+ {
+ "id": 138,
+ "displayName": "Beacon",
+ "name": "beacon",
+ "stackSize": 64
+ },
+ {
+ "id": 139,
+ "displayName": "Cobblestone Wall",
+ "name": "cobblestone_wall",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Cobblestone Wall"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Mossy Cobblestone Wall"
+ }
+ ]
+ },
+ {
+ "id": 143,
+ "displayName": "Wooden Button",
+ "name": "wooden_button",
+ "stackSize": 64
+ },
+ {
+ "id": 145,
+ "displayName": "Anvil",
+ "name": "anvil",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Anvil"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Slightly Damaged Anvil"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Very Damaged Anvil"
+ }
+ ]
+ },
+ {
+ "id": 146,
+ "displayName": "Trapped Chest",
+ "name": "trapped_chest",
+ "stackSize": 64
+ },
+ {
+ "id": 147,
+ "displayName": "Weighted Pressure Plate (Light)",
+ "name": "light_weighted_pressure_plate",
+ "stackSize": 64
+ },
+ {
+ "id": 148,
+ "displayName": "Weighted Pressure Plate (Heavy)",
+ "name": "heavy_weighted_pressure_plate",
+ "stackSize": 64
+ },
+ {
+ "id": 151,
+ "displayName": "Daylight Detector",
+ "name": "daylight_detector",
+ "stackSize": 64
+ },
+ {
+ "id": 152,
+ "displayName": "Block of Redstone",
+ "name": "redstone_block",
+ "stackSize": 64
+ },
+ {
+ "id": 153,
+ "displayName": "Nether Quartz",
+ "name": "quartz_ore",
+ "stackSize": 64
+ },
+ {
+ "id": 154,
+ "displayName": "Hopper",
+ "name": "hopper",
+ "stackSize": 64
+ },
+ {
+ "id": 155,
+ "displayName": "Block of Quartz",
+ "name": "quartz_block",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Block of Quartz"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Chiseled Quartz Block"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Pillar Quartz Block"
+ }
+ ]
+ },
+ {
+ "id": 156,
+ "displayName": "Quartz Stairs",
+ "name": "quartz_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 157,
+ "displayName": "Activator Rail",
+ "name": "activator_rail",
+ "stackSize": 64
+ },
+ {
+ "id": 158,
+ "displayName": "Dropper",
+ "name": "dropper",
+ "stackSize": 64
+ },
+ {
+ "id": 159,
+ "displayName": "Stained Clay",
+ "name": "stained_hardened_clay",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "White Stained Clay"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Orange Stained Clay"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Magenta Stained Clay"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Light Blue Stained Clay"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Yellow Stained Clay"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Lime Stained Clay"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Pink Stained Clay"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Gray Stained Clay"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Light Gray Stained Clay"
+ },
+ {
+ "metadata": 9,
+ "displayName": "Cyan Stained Clay"
+ },
+ {
+ "metadata": 10,
+ "displayName": "Purple Stained Clay"
+ },
+ {
+ "metadata": 11,
+ "displayName": "Blue Stained Clay"
+ },
+ {
+ "metadata": 12,
+ "displayName": "Brown Stained Clay"
+ },
+ {
+ "metadata": 13,
+ "displayName": "Green Stained Clay"
+ },
+ {
+ "metadata": 14,
+ "displayName": "Red Stained Clay"
+ },
+ {
+ "metadata": 15,
+ "displayName": "Black Stained Clay"
+ }
+ ]
+ },
+ {
+ "id": 160,
+ "displayName": "Stained Glass Pane",
+ "name": "stained_glass_pane",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "White Stained Glass Pane"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Orange Stained Glass Pane"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Magenta Stained Glass Pane"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Light Blue Stained Glass Pane"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Yellow Stained Glass Pane"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Lime Stained Glass Pane"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Pink Stained Glass Pane"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Gray Stained Glass Pane"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Light Gray Stained Glass Pane"
+ },
+ {
+ "metadata": 9,
+ "displayName": "Cyan Stained Glass Pane"
+ },
+ {
+ "metadata": 10,
+ "displayName": "Purple Stained Glass Pane"
+ },
+ {
+ "metadata": 11,
+ "displayName": "Blue Stained Glass Pane"
+ },
+ {
+ "metadata": 12,
+ "displayName": "Brown Stained Glass Pane"
+ },
+ {
+ "metadata": 13,
+ "displayName": "Green Stained Glass Pane"
+ },
+ {
+ "metadata": 14,
+ "displayName": "Red Stained Glass Pane"
+ },
+ {
+ "metadata": 15,
+ "displayName": "Black Stained Glass Pane"
+ }
+ ]
+ },
+ {
+ "id": 161,
+ "displayName": "Leaves",
+ "name": "leaves2",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Acacia Leaves"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Dark Oak Leaves"
+ }
+ ]
+ },
+ {
+ "id": 162,
+ "displayName": "Wood",
+ "name": "log2",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Acacia Wood"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Dark Oak Wood"
+ }
+ ]
+ },
+ {
+ "id": 163,
+ "displayName": "Acacia Wood Stairs",
+ "name": "acacia_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 164,
+ "displayName": "Dark Oak Wood Stairs",
+ "name": "dark_oak_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 165,
+ "displayName": "Slime Block",
+ "name": "slime",
+ "stackSize": 64
+ },
+ {
+ "id": 166,
+ "displayName": "Barrier",
+ "name": "barrier",
+ "stackSize": 64
+ },
+ {
+ "id": 167,
+ "displayName": "Iron Trapdoor",
+ "name": "iron_trapdoor",
+ "stackSize": 64
+ },
+ {
+ "id": 168,
+ "displayName": "Prismarine",
+ "name": "prismarine",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Prismarine"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Prismarine Bricks"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Dark Prismarine"
+ }
+ ]
+ },
+ {
+ "id": 169,
+ "displayName": "Sea Lantern",
+ "name": "sea_lantern",
+ "stackSize": 64
+ },
+ {
+ "id": 170,
+ "displayName": "Hay Bale",
+ "name": "hay_block",
+ "stackSize": 64
+ },
+ {
+ "id": 171,
+ "displayName": "Carpet",
+ "name": "carpet",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "White Carpet"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Orange Carpet"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Magenta Carpet"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Light Blue Carpet"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Yellow Carpet"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Lime Carpet"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Pink Carpet"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Gray Carpet"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Light Gray Carpet"
+ },
+ {
+ "metadata": 9,
+ "displayName": "Cyan Carpet"
+ },
+ {
+ "metadata": 10,
+ "displayName": "Purple Carpet"
+ },
+ {
+ "metadata": 11,
+ "displayName": "Blue Carpet"
+ },
+ {
+ "metadata": 12,
+ "displayName": "Brown Carpet"
+ },
+ {
+ "metadata": 13,
+ "displayName": "Green Carpet"
+ },
+ {
+ "metadata": 14,
+ "displayName": "Red Carpet"
+ },
+ {
+ "metadata": 15,
+ "displayName": "Black Carpet"
+ }
+ ]
+ },
+ {
+ "id": 172,
+ "displayName": "Hardened Clay",
+ "name": "hardened_clay",
+ "stackSize": 64
+ },
+ {
+ "id": 173,
+ "displayName": "Block of Coal",
+ "name": "coal_block",
+ "stackSize": 64
+ },
+ {
+ "id": 174,
+ "displayName": "Packed Ice",
+ "name": "packed_ice",
+ "stackSize": 64
+ },
+ {
+ "id": 175,
+ "displayName": "Large Flowers",
+ "name": "double_plant",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Sunflower"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Lilac"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Double Tallgrass"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Large Fern"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Rose Bush"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Peony"
+ }
+ ]
+ },
+ {
+ "id": 179,
+ "displayName": "Red Sandstone",
+ "name": "red_sandstone",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Red Sandstone"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Chiseled Red Sandstone"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Smooth Red Sandstone"
+ }
+ ]
+ },
+ {
+ "id": 180,
+ "displayName": "Red Sandstone Stairs",
+ "name": "red_sandstone_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 182,
+ "displayName": "Red Sandstone Slab",
+ "name": "stone_slab2",
+ "stackSize": 64
+ },
+ {
+ "id": 183,
+ "displayName": "Spruce Fence Gate",
+ "name": "spruce_fence_gate",
+ "stackSize": 64
+ },
+ {
+ "id": 184,
+ "displayName": "Birch Fence Gate",
+ "name": "birch_fence_gate",
+ "stackSize": 64
+ },
+ {
+ "id": 185,
+ "displayName": "Jungle Fence Gate",
+ "name": "jungle_fence_gate",
+ "stackSize": 64
+ },
+ {
+ "id": 186,
+ "displayName": "Dark Oak Fence Gate",
+ "name": "dark_oak_fence_gate",
+ "stackSize": 64
+ },
+ {
+ "id": 187,
+ "displayName": "Acacia Fence Gate",
+ "name": "acacia_fence_gate",
+ "stackSize": 64
+ },
+ {
+ "id": 188,
+ "displayName": "Spruce Fence",
+ "name": "spruce_fence",
+ "stackSize": 64
+ },
+ {
+ "id": 189,
+ "displayName": "Birch Fence",
+ "name": "birch_fence",
+ "stackSize": 64
+ },
+ {
+ "id": 190,
+ "displayName": "Jungle Fence",
+ "name": "jungle_fence",
+ "stackSize": 64
+ },
+ {
+ "id": 191,
+ "displayName": "Dark Oak Fence",
+ "name": "dark_oak_fence",
+ "stackSize": 64
+ },
+ {
+ "id": 192,
+ "displayName": "Acacia Fence",
+ "name": "acacia_fence",
+ "stackSize": 64
+ },
+ {
+ "id": 256,
+ "displayName": "Iron Shovel",
+ "name": "iron_shovel",
+ "stackSize": 1,
+ "maxDurability": 250,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 257,
+ "displayName": "Iron Pickaxe",
+ "name": "iron_pickaxe",
+ "stackSize": 1,
+ "maxDurability": 250,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 258,
+ "displayName": "Iron Axe",
+ "name": "iron_axe",
+ "stackSize": 1,
+ "maxDurability": 250,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 259,
+ "displayName": "Flint and Steel",
+ "name": "flint_and_steel",
+ "stackSize": 1,
+ "maxDurability": 64,
+ "enchantCategories": [
+ "breakable",
+ "vanishable"
+ ]
+ },
+ {
+ "id": 260,
+ "displayName": "Apple",
+ "name": "apple",
+ "stackSize": 64
+ },
+ {
+ "id": 261,
+ "displayName": "Bow",
+ "name": "bow",
+ "stackSize": 1,
+ "maxDurability": 384,
+ "enchantCategories": [
+ "breakable",
+ "bow",
+ "vanishable"
+ ]
+ },
+ {
+ "id": 262,
+ "displayName": "Arrow",
+ "name": "arrow",
+ "stackSize": 64
+ },
+ {
+ "id": 263,
+ "displayName": "Coal",
+ "name": "coal",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Coal"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Charcoal"
+ }
+ ]
+ },
+ {
+ "id": 264,
+ "displayName": "Diamond",
+ "name": "diamond",
+ "stackSize": 64
+ },
+ {
+ "id": 265,
+ "displayName": "Iron Ingot",
+ "name": "iron_ingot",
+ "stackSize": 64
+ },
+ {
+ "id": 266,
+ "displayName": "Gold Ingot",
+ "name": "gold_ingot",
+ "stackSize": 64
+ },
+ {
+ "id": 267,
+ "displayName": "Iron Sword",
+ "name": "iron_sword",
+ "stackSize": 1,
+ "maxDurability": 250,
+ "enchantCategories": [
+ "weapon",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 268,
+ "displayName": "Wooden Sword",
+ "name": "wooden_sword",
+ "stackSize": 1,
+ "maxDurability": 59,
+ "enchantCategories": [
+ "weapon",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "oak_planks",
+ "spruce_planks",
+ "birch_planks",
+ "jungle_planks",
+ "acacia_planks",
+ "dark_oak_planks",
+ "crimson_planks",
+ "warped_planks"
+ ]
+ },
+ {
+ "id": 269,
+ "displayName": "Wooden Shovel",
+ "name": "wooden_shovel",
+ "stackSize": 1,
+ "maxDurability": 59,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "oak_planks",
+ "spruce_planks",
+ "birch_planks",
+ "jungle_planks",
+ "acacia_planks",
+ "dark_oak_planks",
+ "crimson_planks",
+ "warped_planks"
+ ]
+ },
+ {
+ "id": 270,
+ "displayName": "Wooden Pickaxe",
+ "name": "wooden_pickaxe",
+ "stackSize": 1,
+ "maxDurability": 59,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "oak_planks",
+ "spruce_planks",
+ "birch_planks",
+ "jungle_planks",
+ "acacia_planks",
+ "dark_oak_planks",
+ "crimson_planks",
+ "warped_planks"
+ ]
+ },
+ {
+ "id": 271,
+ "displayName": "Wooden Axe",
+ "name": "wooden_axe",
+ "stackSize": 1,
+ "maxDurability": 59,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "oak_planks",
+ "spruce_planks",
+ "birch_planks",
+ "jungle_planks",
+ "acacia_planks",
+ "dark_oak_planks",
+ "crimson_planks",
+ "warped_planks"
+ ]
+ },
+ {
+ "id": 272,
+ "displayName": "Stone Sword",
+ "name": "stone_sword",
+ "stackSize": 1,
+ "maxDurability": 131,
+ "enchantCategories": [
+ "weapon",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "cobblestone",
+ "blackstone"
+ ]
+ },
+ {
+ "id": 273,
+ "displayName": "Stone Shovel",
+ "name": "stone_shovel",
+ "stackSize": 1,
+ "maxDurability": 131,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "cobblestone",
+ "blackstone"
+ ]
+ },
+ {
+ "id": 274,
+ "displayName": "Stone Pickaxe",
+ "name": "stone_pickaxe",
+ "stackSize": 1,
+ "maxDurability": 131,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "cobblestone",
+ "blackstone"
+ ]
+ },
+ {
+ "id": 275,
+ "displayName": "Stone Axe",
+ "name": "stone_axe",
+ "stackSize": 1,
+ "maxDurability": 131,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "cobblestone",
+ "blackstone"
+ ]
+ },
+ {
+ "id": 276,
+ "displayName": "Diamond Sword",
+ "name": "diamond_sword",
+ "stackSize": 1,
+ "maxDurability": 1561,
+ "enchantCategories": [
+ "weapon",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 277,
+ "displayName": "Diamond Shovel",
+ "name": "diamond_shovel",
+ "stackSize": 1,
+ "maxDurability": 1561,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 278,
+ "displayName": "Diamond Pickaxe",
+ "name": "diamond_pickaxe",
+ "stackSize": 1,
+ "maxDurability": 1561,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 279,
+ "displayName": "Diamond Axe",
+ "name": "diamond_axe",
+ "stackSize": 1,
+ "maxDurability": 1561,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 280,
+ "displayName": "Stick",
+ "name": "stick",
+ "stackSize": 64
+ },
+ {
+ "id": 281,
+ "displayName": "Bowl",
+ "name": "bowl",
+ "stackSize": 64
+ },
+ {
+ "id": 282,
+ "displayName": "Mushroom Stew",
+ "name": "mushroom_stew",
+ "stackSize": 1
+ },
+ {
+ "id": 283,
+ "displayName": "Golden Sword",
+ "name": "golden_sword",
+ "stackSize": 1,
+ "maxDurability": 32,
+ "enchantCategories": [
+ "weapon",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 284,
+ "displayName": "Golden Shovel",
+ "name": "golden_shovel",
+ "stackSize": 1,
+ "maxDurability": 32,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 285,
+ "displayName": "Golden Pickaxe",
+ "name": "golden_pickaxe",
+ "stackSize": 1,
+ "maxDurability": 32,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 286,
+ "displayName": "Golden Axe",
+ "name": "golden_axe",
+ "stackSize": 1,
+ "maxDurability": 32,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 287,
+ "displayName": "String",
+ "name": "string",
+ "stackSize": 64
+ },
+ {
+ "id": 288,
+ "displayName": "Feather",
+ "name": "feather",
+ "stackSize": 64
+ },
+ {
+ "id": 289,
+ "displayName": "Gunpowder",
+ "name": "gunpowder",
+ "stackSize": 64
+ },
+ {
+ "id": 290,
+ "displayName": "Wooden Hoe",
+ "name": "wooden_hoe",
+ "stackSize": 1,
+ "maxDurability": 59,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "oak_planks",
+ "spruce_planks",
+ "birch_planks",
+ "jungle_planks",
+ "acacia_planks",
+ "dark_oak_planks",
+ "crimson_planks",
+ "warped_planks"
+ ]
+ },
+ {
+ "id": 291,
+ "displayName": "Stone Hoe",
+ "name": "stone_hoe",
+ "stackSize": 1,
+ "maxDurability": 131,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "cobblestone",
+ "blackstone"
+ ]
+ },
+ {
+ "id": 292,
+ "displayName": "Iron Hoe",
+ "name": "iron_hoe",
+ "stackSize": 1,
+ "maxDurability": 250,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 293,
+ "displayName": "Diamond Hoe",
+ "name": "diamond_hoe",
+ "stackSize": 1,
+ "maxDurability": 1561,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 294,
+ "displayName": "Golden Hoe",
+ "name": "golden_hoe",
+ "stackSize": 1,
+ "maxDurability": 32,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 295,
+ "displayName": "Seeds",
+ "name": "wheat_seeds",
+ "stackSize": 64
+ },
+ {
+ "id": 296,
+ "displayName": "Wheat",
+ "name": "wheat",
+ "stackSize": 64
+ },
+ {
+ "id": 297,
+ "displayName": "Bread",
+ "name": "bread",
+ "stackSize": 64
+ },
+ {
+ "id": 298,
+ "displayName": "Leather Cap",
+ "name": "leather_helmet",
+ "stackSize": 1,
+ "maxDurability": 55,
+ "enchantCategories": [
+ "armor",
+ "armor_head",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "leather"
+ ]
+ },
+ {
+ "id": 299,
+ "displayName": "Leather Tunic",
+ "name": "leather_chestplate",
+ "stackSize": 1,
+ "maxDurability": 80,
+ "enchantCategories": [
+ "armor",
+ "armor_chest",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "leather"
+ ]
+ },
+ {
+ "id": 300,
+ "displayName": "Leather Pants",
+ "name": "leather_leggings",
+ "stackSize": 1,
+ "maxDurability": 75,
+ "enchantCategories": [
+ "armor",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "leather"
+ ]
+ },
+ {
+ "id": 301,
+ "displayName": "Leather Boots",
+ "name": "leather_boots",
+ "stackSize": 1,
+ "maxDurability": 65,
+ "enchantCategories": [
+ "armor",
+ "armor_feet",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "leather"
+ ]
+ },
+ {
+ "id": 302,
+ "displayName": "Chain Helmet",
+ "name": "chainmail_helmet",
+ "stackSize": 1,
+ "maxDurability": 165,
+ "enchantCategories": [
+ "armor",
+ "armor_head",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 303,
+ "displayName": "Chain Chestplate",
+ "name": "chainmail_chestplate",
+ "stackSize": 1,
+ "maxDurability": 240,
+ "enchantCategories": [
+ "armor",
+ "armor_chest",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 304,
+ "displayName": "Chain Leggings",
+ "name": "chainmail_leggings",
+ "stackSize": 1,
+ "maxDurability": 225,
+ "enchantCategories": [
+ "armor",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 305,
+ "displayName": "Chain Boots",
+ "name": "chainmail_boots",
+ "stackSize": 1,
+ "maxDurability": 195,
+ "enchantCategories": [
+ "armor",
+ "armor_feet",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 306,
+ "displayName": "Iron Helmet",
+ "name": "iron_helmet",
+ "stackSize": 1,
+ "maxDurability": 165,
+ "enchantCategories": [
+ "armor",
+ "armor_head",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 307,
+ "displayName": "Iron Chestplate",
+ "name": "iron_chestplate",
+ "stackSize": 1,
+ "maxDurability": 240,
+ "enchantCategories": [
+ "armor",
+ "armor_chest",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 308,
+ "displayName": "Iron Leggings",
+ "name": "iron_leggings",
+ "stackSize": 1,
+ "maxDurability": 225,
+ "enchantCategories": [
+ "armor",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 309,
+ "displayName": "Iron Boots",
+ "name": "iron_boots",
+ "stackSize": 1,
+ "maxDurability": 195,
+ "enchantCategories": [
+ "armor",
+ "armor_feet",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 310,
+ "displayName": "Diamond Helmet",
+ "name": "diamond_helmet",
+ "stackSize": 1,
+ "maxDurability": 363,
+ "enchantCategories": [
+ "armor",
+ "armor_head",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 311,
+ "displayName": "Diamond Chestplate",
+ "name": "diamond_chestplate",
+ "stackSize": 1,
+ "maxDurability": 528,
+ "enchantCategories": [
+ "armor",
+ "armor_chest",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 312,
+ "displayName": "Diamond Leggings",
+ "name": "diamond_leggings",
+ "stackSize": 1,
+ "maxDurability": 495,
+ "enchantCategories": [
+ "armor",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 313,
+ "displayName": "Diamond Boots",
+ "name": "diamond_boots",
+ "stackSize": 1,
+ "maxDurability": 429,
+ "enchantCategories": [
+ "armor",
+ "armor_feet",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 314,
+ "displayName": "Golden Helmet",
+ "name": "golden_helmet",
+ "stackSize": 1,
+ "maxDurability": 77,
+ "enchantCategories": [
+ "armor",
+ "armor_head",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 315,
+ "displayName": "Golden Chestplate",
+ "name": "golden_chestplate",
+ "stackSize": 1,
+ "maxDurability": 112,
+ "enchantCategories": [
+ "armor",
+ "armor_chest",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 316,
+ "displayName": "Golden Leggings",
+ "name": "golden_leggings",
+ "stackSize": 1,
+ "maxDurability": 105,
+ "enchantCategories": [
+ "armor",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 317,
+ "displayName": "Golden Boots",
+ "name": "golden_boots",
+ "stackSize": 1,
+ "maxDurability": 91,
+ "enchantCategories": [
+ "armor",
+ "armor_feet",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 318,
+ "displayName": "Flint",
+ "name": "flint",
+ "stackSize": 64
+ },
+ {
+ "id": 319,
+ "displayName": "Raw Porkchop",
+ "name": "porkchop",
+ "stackSize": 64
+ },
+ {
+ "id": 320,
+ "displayName": "Cooked Porkchop",
+ "name": "cooked_porkchop",
+ "stackSize": 64
+ },
+ {
+ "id": 321,
+ "displayName": "Painting",
+ "name": "painting",
+ "stackSize": 64
+ },
+ {
+ "id": 322,
+ "displayName": "Golden Apple",
+ "name": "golden_apple",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Golden Apple"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Enchanted Golden Apple"
+ }
+ ]
+ },
+ {
+ "id": 323,
+ "displayName": "Sign",
+ "name": "sign",
+ "stackSize": 16
+ },
+ {
+ "id": 324,
+ "displayName": "Oak Door",
+ "name": "wooden_door",
+ "stackSize": 64
+ },
+ {
+ "id": 325,
+ "displayName": "Bucket",
+ "name": "bucket",
+ "stackSize": 16
+ },
+ {
+ "id": 326,
+ "displayName": "Water Bucket",
+ "name": "water_bucket",
+ "stackSize": 1
+ },
+ {
+ "id": 327,
+ "displayName": "Lava Bucket",
+ "name": "lava_bucket",
+ "stackSize": 1
+ },
+ {
+ "id": 328,
+ "displayName": "Minecart",
+ "name": "minecart",
+ "stackSize": 1
+ },
+ {
+ "id": 329,
+ "displayName": "Saddle",
+ "name": "saddle",
+ "stackSize": 1
+ },
+ {
+ "id": 330,
+ "displayName": "Iron Door",
+ "name": "iron_door",
+ "stackSize": 64
+ },
+ {
+ "id": 331,
+ "displayName": "Redstone",
+ "name": "redstone",
+ "stackSize": 64
+ },
+ {
+ "id": 332,
+ "displayName": "Snowball",
+ "name": "snowball",
+ "stackSize": 16
+ },
+ {
+ "id": 333,
+ "displayName": "Boat",
+ "name": "boat",
+ "stackSize": 1
+ },
+ {
+ "id": 334,
+ "displayName": "Leather",
+ "name": "leather",
+ "stackSize": 64
+ },
+ {
+ "id": 335,
+ "displayName": "Milk",
+ "name": "milk_bucket",
+ "stackSize": 1
+ },
+ {
+ "id": 336,
+ "displayName": "Brick",
+ "name": "brick",
+ "stackSize": 64
+ },
+ {
+ "id": 337,
+ "displayName": "Clay",
+ "name": "clay_ball",
+ "stackSize": 64
+ },
+ {
+ "id": 338,
+ "displayName": "Sugar Canes",
+ "name": "reeds",
+ "stackSize": 64
+ },
+ {
+ "id": 339,
+ "displayName": "Paper",
+ "name": "paper",
+ "stackSize": 64
+ },
+ {
+ "id": 340,
+ "displayName": "Book",
+ "name": "book",
+ "stackSize": 64
+ },
+ {
+ "id": 341,
+ "displayName": "Slimeball",
+ "name": "slime_ball",
+ "stackSize": 64
+ },
+ {
+ "id": 342,
+ "displayName": "Minecart with Chest",
+ "name": "chest_minecart",
+ "stackSize": 1
+ },
+ {
+ "id": 343,
+ "displayName": "Minecart with Furnace",
+ "name": "furnace_minecart",
+ "stackSize": 1
+ },
+ {
+ "id": 344,
+ "displayName": "Egg",
+ "name": "egg",
+ "stackSize": 16
+ },
+ {
+ "id": 345,
+ "displayName": "Compass",
+ "name": "compass",
+ "stackSize": 64
+ },
+ {
+ "id": 346,
+ "displayName": "Fishing Rod",
+ "name": "fishing_rod",
+ "stackSize": 1,
+ "maxDurability": 64,
+ "enchantCategories": [
+ "breakable",
+ "fishing_rod",
+ "vanishable"
+ ]
+ },
+ {
+ "id": 347,
+ "displayName": "Clock",
+ "name": "clock",
+ "stackSize": 64
+ },
+ {
+ "id": 348,
+ "displayName": "Glowstone Dust",
+ "name": "glowstone_dust",
+ "stackSize": 64
+ },
+ {
+ "id": 349,
+ "displayName": "Fish",
+ "name": "fish",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Raw Fish"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Raw Salmon"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Clownfish"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Pufferfish"
+ }
+ ]
+ },
+ {
+ "id": 350,
+ "displayName": "Cooked Fish",
+ "name": "cooked_fish",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Cooked Fish"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Cooked Salmon"
+ }
+ ]
+ },
+ {
+ "id": 351,
+ "displayName": "Dye",
+ "name": "dye",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Ink Sac"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Rose Red"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Cactus Green"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Cocoa Beans"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Lapis Lazuli"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Purple Dye"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Cyan Dye"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Light Gray Dye"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Gray Dye"
+ },
+ {
+ "metadata": 9,
+ "displayName": "Pink Dye"
+ },
+ {
+ "metadata": 10,
+ "displayName": "Lime Dye"
+ },
+ {
+ "metadata": 11,
+ "displayName": "Dandelion Yellow"
+ },
+ {
+ "metadata": 12,
+ "displayName": "Light Blue Dye"
+ },
+ {
+ "metadata": 13,
+ "displayName": "Magenta Dye"
+ },
+ {
+ "metadata": 14,
+ "displayName": "Orange Dye"
+ },
+ {
+ "metadata": 15,
+ "displayName": "Bone Meal"
+ }
+ ]
+ },
+ {
+ "id": 352,
+ "displayName": "Bone",
+ "name": "bone",
+ "stackSize": 64
+ },
+ {
+ "id": 353,
+ "displayName": "Sugar",
+ "name": "sugar",
+ "stackSize": 64
+ },
+ {
+ "id": 354,
+ "displayName": "Cake",
+ "name": "cake",
+ "stackSize": 1
+ },
+ {
+ "id": 355,
+ "displayName": "Bed",
+ "name": "bed",
+ "stackSize": 1
+ },
+ {
+ "id": 356,
+ "displayName": "Redstone Repeater",
+ "name": "repeater",
+ "stackSize": 64
+ },
+ {
+ "id": 357,
+ "displayName": "Cookie",
+ "name": "cookie",
+ "stackSize": 64
+ },
+ {
+ "id": 358,
+ "displayName": "Map",
+ "name": "filled_map",
+ "stackSize": 64
+ },
+ {
+ "id": 359,
+ "displayName": "Shears",
+ "name": "shears",
+ "stackSize": 1,
+ "maxDurability": 238,
+ "enchantCategories": [
+ "breakable",
+ "vanishable"
+ ]
+ },
+ {
+ "id": 360,
+ "displayName": "Melon",
+ "name": "melon",
+ "stackSize": 64
+ },
+ {
+ "id": 361,
+ "displayName": "Pumpkin Seeds",
+ "name": "pumpkin_seeds",
+ "stackSize": 64
+ },
+ {
+ "id": 362,
+ "displayName": "Melon Seeds",
+ "name": "melon_seeds",
+ "stackSize": 64
+ },
+ {
+ "id": 363,
+ "displayName": "Raw Beef",
+ "name": "beef",
+ "stackSize": 64
+ },
+ {
+ "id": 364,
+ "displayName": "Steak",
+ "name": "cooked_beef",
+ "stackSize": 64
+ },
+ {
+ "id": 365,
+ "displayName": "Raw Chicken",
+ "name": "chicken",
+ "stackSize": 64
+ },
+ {
+ "id": 366,
+ "displayName": "Cooked Chicken",
+ "name": "cooked_chicken",
+ "stackSize": 64
+ },
+ {
+ "id": 367,
+ "displayName": "Rotten Flesh",
+ "name": "rotten_flesh",
+ "stackSize": 64
+ },
+ {
+ "id": 368,
+ "displayName": "Ender Pearl",
+ "name": "ender_pearl",
+ "stackSize": 16
+ },
+ {
+ "id": 369,
+ "displayName": "Blaze Rod",
+ "name": "blaze_rod",
+ "stackSize": 64
+ },
+ {
+ "id": 370,
+ "displayName": "Ghast Tear",
+ "name": "ghast_tear",
+ "stackSize": 64
+ },
+ {
+ "id": 371,
+ "displayName": "Gold Nugget",
+ "name": "gold_nugget",
+ "stackSize": 64
+ },
+ {
+ "id": 372,
+ "displayName": "Nether Wart",
+ "name": "nether_wart",
+ "stackSize": 64
+ },
+ {
+ "id": 373,
+ "displayName": "Potion",
+ "name": "potion",
+ "stackSize": 1
+ },
+ {
+ "id": 374,
+ "displayName": "Glass Bottle",
+ "name": "glass_bottle",
+ "stackSize": 64
+ },
+ {
+ "id": 375,
+ "displayName": "Spider Eye",
+ "name": "spider_eye",
+ "stackSize": 64
+ },
+ {
+ "id": 376,
+ "displayName": "Fermented Spider Eye",
+ "name": "fermented_spider_eye",
+ "stackSize": 64
+ },
+ {
+ "id": 377,
+ "displayName": "Blaze Powder",
+ "name": "blaze_powder",
+ "stackSize": 64
+ },
+ {
+ "id": 378,
+ "displayName": "Magma Cream",
+ "name": "magma_cream",
+ "stackSize": 64
+ },
+ {
+ "id": 379,
+ "displayName": "Brewing Stand",
+ "name": "brewing_stand",
+ "stackSize": 64
+ },
+ {
+ "id": 380,
+ "displayName": "Cauldron",
+ "name": "cauldron",
+ "stackSize": 64
+ },
+ {
+ "id": 381,
+ "displayName": "Eye of Ender",
+ "name": "ender_eye",
+ "stackSize": 64
+ },
+ {
+ "id": 382,
+ "displayName": "Glistering Melon",
+ "name": "speckled_melon",
+ "stackSize": 64
+ },
+ {
+ "id": 383,
+ "displayName": "Spawn Egg",
+ "name": "spawn_egg",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Spawn"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Spawn Dropped item"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Spawn Thrown egg"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Spawn Lead knot"
+ },
+ {
+ "metadata": 10,
+ "displayName": "Spawn Shot arrow"
+ },
+ {
+ "metadata": 11,
+ "displayName": "Spawn Thrown snowball"
+ },
+ {
+ "metadata": 12,
+ "displayName": "Spawn Ghast fireball"
+ },
+ {
+ "metadata": 13,
+ "displayName": "Spawn Blaze fireball"
+ },
+ {
+ "metadata": 14,
+ "displayName": "Spawn Thrown Ender Pearl"
+ },
+ {
+ "metadata": 15,
+ "displayName": "Spawn Thrown Eye of Ender"
+ },
+ {
+ "metadata": 16,
+ "displayName": "Spawn Thrown splash potion"
+ },
+ {
+ "metadata": 17,
+ "displayName": "Spawn Thrown Bottle o' Enchanting"
+ },
+ {
+ "metadata": 18,
+ "displayName": "Spawn Item Frame"
+ },
+ {
+ "metadata": 19,
+ "displayName": "Spawn Wither Skull"
+ },
+ {
+ "metadata": 20,
+ "displayName": "Spawn Primed TNT"
+ },
+ {
+ "metadata": 21,
+ "displayName": "Spawn Falling block"
+ },
+ {
+ "metadata": 21,
+ "displayName": "Spawn Falling block"
+ },
+ {
+ "metadata": 22,
+ "displayName": "Spawn Firework Rocket"
+ },
+ {
+ "metadata": 30,
+ "displayName": "Spawn Armor Stand"
+ },
+ {
+ "metadata": 41,
+ "displayName": "Spawn Boat"
+ },
+ {
+ "metadata": 42,
+ "displayName": "Spawn Minecart"
+ },
+ {
+ "metadata": 42,
+ "displayName": "Spawn Minecart"
+ },
+ {
+ "metadata": 42,
+ "displayName": "Spawn Minecart"
+ },
+ {
+ "metadata": 48,
+ "displayName": "Spawn Mob"
+ },
+ {
+ "metadata": 49,
+ "displayName": "Spawn Monster"
+ },
+ {
+ "metadata": 50,
+ "displayName": "Spawn Creeper"
+ },
+ {
+ "metadata": 51,
+ "displayName": "Spawn Skeleton"
+ },
+ {
+ "metadata": 52,
+ "displayName": "Spawn Spider"
+ },
+ {
+ "metadata": 53,
+ "displayName": "Spawn Giant"
+ },
+ {
+ "metadata": 54,
+ "displayName": "Spawn Zombie"
+ },
+ {
+ "metadata": 55,
+ "displayName": "Spawn Slime"
+ },
+ {
+ "metadata": 56,
+ "displayName": "Spawn Ghast"
+ },
+ {
+ "metadata": 57,
+ "displayName": "Spawn Zombie Pigman"
+ },
+ {
+ "metadata": 58,
+ "displayName": "Spawn Enderman"
+ },
+ {
+ "metadata": 59,
+ "displayName": "Spawn Cave Spider"
+ },
+ {
+ "metadata": 60,
+ "displayName": "Spawn Silverfish"
+ },
+ {
+ "metadata": 61,
+ "displayName": "Spawn Blaze"
+ },
+ {
+ "metadata": 62,
+ "displayName": "Spawn Magma Cube"
+ },
+ {
+ "metadata": 63,
+ "displayName": "Spawn Ender Dragon"
+ },
+ {
+ "metadata": 64,
+ "displayName": "Spawn Wither"
+ },
+ {
+ "metadata": 65,
+ "displayName": "Spawn Bat"
+ },
+ {
+ "metadata": 66,
+ "displayName": "Spawn Witch"
+ },
+ {
+ "metadata": 67,
+ "displayName": "Spawn Endermite"
+ },
+ {
+ "metadata": 68,
+ "displayName": "Spawn Guardian"
+ },
+ {
+ "metadata": 90,
+ "displayName": "Spawn Pig"
+ },
+ {
+ "metadata": 91,
+ "displayName": "Spawn Sheep"
+ },
+ {
+ "metadata": 92,
+ "displayName": "Spawn Cow"
+ },
+ {
+ "metadata": 93,
+ "displayName": "Spawn Chicken"
+ },
+ {
+ "metadata": 94,
+ "displayName": "Spawn Squid"
+ },
+ {
+ "metadata": 95,
+ "displayName": "Spawn Wolf"
+ },
+ {
+ "metadata": 96,
+ "displayName": "Spawn Mooshroom"
+ },
+ {
+ "metadata": 97,
+ "displayName": "Spawn Snow Golem"
+ },
+ {
+ "metadata": 98,
+ "displayName": "Spawn Ocelot"
+ },
+ {
+ "metadata": 99,
+ "displayName": "Spawn Iron Golem"
+ },
+ {
+ "metadata": 100,
+ "displayName": "Spawn Horse"
+ },
+ {
+ "metadata": 101,
+ "displayName": "Spawn Rabbit"
+ },
+ {
+ "metadata": 120,
+ "displayName": "Spawn Villager"
+ },
+ {
+ "metadata": 200,
+ "displayName": "Spawn Ender Crystal"
+ }
+ ]
+ },
+ {
+ "id": 384,
+ "displayName": "Bottle o' Enchanting",
+ "name": "experience_bottle",
+ "stackSize": 64
+ },
+ {
+ "id": 385,
+ "displayName": "Fire Charge",
+ "name": "fire_charge",
+ "stackSize": 64
+ },
+ {
+ "id": 386,
+ "displayName": "Book and Quill",
+ "name": "writable_book",
+ "stackSize": 1
+ },
+ {
+ "id": 387,
+ "displayName": "Written Book",
+ "name": "written_book",
+ "stackSize": 16
+ },
+ {
+ "id": 388,
+ "displayName": "Emerald",
+ "name": "emerald",
+ "stackSize": 64
+ },
+ {
+ "id": 389,
+ "displayName": "Item Frame",
+ "name": "item_frame",
+ "stackSize": 64
+ },
+ {
+ "id": 390,
+ "displayName": "Flower Pot",
+ "name": "flower_pot",
+ "stackSize": 64
+ },
+ {
+ "id": 391,
+ "displayName": "Carrot",
+ "name": "carrot",
+ "stackSize": 64
+ },
+ {
+ "id": 392,
+ "displayName": "Potato",
+ "name": "potato",
+ "stackSize": 64
+ },
+ {
+ "id": 393,
+ "displayName": "Baked Potato",
+ "name": "baked_potato",
+ "stackSize": 64
+ },
+ {
+ "id": 394,
+ "displayName": "Poisonous Potato",
+ "name": "poisonous_potato",
+ "stackSize": 64
+ },
+ {
+ "id": 395,
+ "displayName": "Empty Map",
+ "name": "map",
+ "stackSize": 64
+ },
+ {
+ "id": 396,
+ "displayName": "Golden Carrot",
+ "name": "golden_carrot",
+ "stackSize": 64
+ },
+ {
+ "id": 397,
+ "displayName": "Skull",
+ "name": "skull",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Skeleton Skull"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Wither Skeleton Skull"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Zombie Head"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Head"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Creeper Head"
+ }
+ ]
+ },
+ {
+ "id": 398,
+ "displayName": "Carrot on a Stick",
+ "name": "carrot_on_a_stick",
+ "stackSize": 1,
+ "maxDurability": 25,
+ "enchantCategories": [
+ "breakable",
+ "vanishable"
+ ]
+ },
+ {
+ "id": 399,
+ "displayName": "Nether Star",
+ "name": "nether_star",
+ "stackSize": 64
+ },
+ {
+ "id": 400,
+ "displayName": "Pumpkin Pie",
+ "name": "pumpkin_pie",
+ "stackSize": 64
+ },
+ {
+ "id": 401,
+ "displayName": "Firework Rocket",
+ "name": "fireworks",
+ "stackSize": 64
+ },
+ {
+ "id": 402,
+ "displayName": "Firework Star",
+ "name": "firework_charge",
+ "stackSize": 64
+ },
+ {
+ "id": 403,
+ "displayName": "Enchanted Book",
+ "name": "enchanted_book",
+ "stackSize": 1
+ },
+ {
+ "id": 404,
+ "displayName": "Redstone Comparator",
+ "name": "comparator",
+ "stackSize": 64
+ },
+ {
+ "id": 405,
+ "displayName": "Nether Brick",
+ "name": "netherbrick",
+ "stackSize": 64
+ },
+ {
+ "id": 406,
+ "displayName": "Nether Quartz",
+ "name": "quartz",
+ "stackSize": 64
+ },
+ {
+ "id": 407,
+ "displayName": "Minecart with TNT",
+ "name": "tnt_minecart",
+ "stackSize": 1
+ },
+ {
+ "id": 408,
+ "displayName": "Minecart with Hopper",
+ "name": "hopper_minecart",
+ "stackSize": 1
+ },
+ {
+ "id": 409,
+ "displayName": "Prismarine Shard",
+ "name": "prismarine_shard",
+ "stackSize": 64
+ },
+ {
+ "id": 410,
+ "displayName": "Prismarine Crystals",
+ "name": "prismarine_crystals",
+ "stackSize": 64
+ },
+ {
+ "id": 411,
+ "displayName": "Raw Rabbit",
+ "name": "rabbit",
+ "stackSize": 64
+ },
+ {
+ "id": 412,
+ "displayName": "Cooked Rabbit",
+ "name": "cooked_rabbit",
+ "stackSize": 64
+ },
+ {
+ "id": 413,
+ "displayName": "Rabbit Stew",
+ "name": "rabbit_stew",
+ "stackSize": 1
+ },
+ {
+ "id": 414,
+ "displayName": "Rabbit's Foot",
+ "name": "rabbit_foot",
+ "stackSize": 64
+ },
+ {
+ "id": 415,
+ "displayName": "Rabbit Hide",
+ "name": "rabbit_hide",
+ "stackSize": 64
+ },
+ {
+ "id": 416,
+ "displayName": "Armor Stand",
+ "name": "armor_stand",
+ "stackSize": 16
+ },
+ {
+ "id": 417,
+ "displayName": "Iron Horse Armor",
+ "name": "iron_horse_armor",
+ "stackSize": 1
+ },
+ {
+ "id": 418,
+ "displayName": "Gold Horse Armor",
+ "name": "golden_horse_armor",
+ "stackSize": 1
+ },
+ {
+ "id": 419,
+ "displayName": "Diamond Horse Armor",
+ "name": "diamond_horse_armor",
+ "stackSize": 1
+ },
+ {
+ "id": 420,
+ "displayName": "Lead",
+ "name": "lead",
+ "stackSize": 64
+ },
+ {
+ "id": 421,
+ "displayName": "Name Tag",
+ "name": "name_tag",
+ "stackSize": 64
+ },
+ {
+ "id": 422,
+ "displayName": "Minecart with Command Block",
+ "name": "command_block_minecart",
+ "stackSize": 1
+ },
+ {
+ "id": 423,
+ "displayName": "Raw Mutton",
+ "name": "mutton",
+ "stackSize": 64
+ },
+ {
+ "id": 424,
+ "displayName": "Cooked Mutton",
+ "name": "cooked_mutton",
+ "stackSize": 64
+ },
+ {
+ "id": 425,
+ "displayName": "Banner",
+ "name": "banner",
+ "stackSize": 16,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Black Banner"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Red Banner"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Green Banner"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Brown Banner"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Blue Banner"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Purple Banner"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Cyan Banner"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Light Gray Banner"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Gray Banner"
+ },
+ {
+ "metadata": 9,
+ "displayName": "Pink Banner"
+ },
+ {
+ "metadata": 10,
+ "displayName": "Lime Banner"
+ },
+ {
+ "metadata": 11,
+ "displayName": "Yellow Banner"
+ },
+ {
+ "metadata": 12,
+ "displayName": "Light Blue Banner"
+ },
+ {
+ "metadata": 13,
+ "displayName": "Magenta Banner"
+ },
+ {
+ "metadata": 14,
+ "displayName": "Orange Banner"
+ },
+ {
+ "metadata": 15,
+ "displayName": "White Banner"
+ }
+ ]
+ },
+ {
+ "id": 427,
+ "displayName": "Spruce Door",
+ "name": "spruce_door",
+ "stackSize": 64
+ },
+ {
+ "id": 428,
+ "displayName": "Birch Door",
+ "name": "birch_door",
+ "stackSize": 64
+ },
+ {
+ "id": 429,
+ "displayName": "Jungle Door",
+ "name": "jungle_door",
+ "stackSize": 64
+ },
+ {
+ "id": 430,
+ "displayName": "Acacia Door",
+ "name": "acacia_door",
+ "stackSize": 64
+ },
+ {
+ "id": 431,
+ "displayName": "Dark Oak Door",
+ "name": "dark_oak_door",
+ "stackSize": 64
+ },
+ {
+ "id": 2256,
+ "displayName": "13 Disc",
+ "name": "record_13",
+ "stackSize": 1
+ },
+ {
+ "id": 2257,
+ "displayName": "Cat Disc",
+ "name": "record_cat",
+ "stackSize": 1
+ },
+ {
+ "id": 2258,
+ "displayName": "Blocks Disc",
+ "name": "record_blocks",
+ "stackSize": 1
+ },
+ {
+ "id": 2259,
+ "displayName": "Chirp Disc",
+ "name": "record_chirp",
+ "stackSize": 1
+ },
+ {
+ "id": 2260,
+ "displayName": "Far Disc",
+ "name": "record_far",
+ "stackSize": 1
+ },
+ {
+ "id": 2261,
+ "displayName": "Mall Disc",
+ "name": "record_mall",
+ "stackSize": 1
+ },
+ {
+ "id": 2262,
+ "displayName": "Mellohi Disc",
+ "name": "record_mellohi",
+ "stackSize": 1
+ },
+ {
+ "id": 2263,
+ "displayName": "Stal Disc",
+ "name": "record_stal",
+ "stackSize": 1
+ },
+ {
+ "id": 2264,
+ "displayName": "Strad Disc",
+ "name": "record_strad",
+ "stackSize": 1
+ },
+ {
+ "id": 2265,
+ "displayName": "Ward Disc",
+ "name": "record_ward",
+ "stackSize": 1
+ },
+ {
+ "id": 2266,
+ "displayName": "11 Disc",
+ "name": "record_11",
+ "stackSize": 1
+ },
+ {
+ "id": 2267,
+ "displayName": "Wait Disc",
+ "name": "record_wait",
+ "stackSize": 1
+ }
+] \ No newline at end of file
diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/inventory_button_background.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/inventory_button_background.png
new file mode 100644
index 0000000..46c86f4
--- /dev/null
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/inventory_button_background.png
Binary files differ
diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png
index 1831ef3..1831ef3 100644
--- a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png
Binary files differ
diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png
index 5b774b2..5b774b2 100644
--- a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png
Binary files differ
diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta
index 94b9a1d..94b9a1d 100644
--- a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta
diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png
new file mode 100644
index 0000000..10d41dd
--- /dev/null
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png
Binary files differ
diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta
index 5964a6f..5964a6f 100644
--- a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta
diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png
index 61e9ee5..61e9ee5 100644
--- a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png
Binary files differ
diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta
index cd2857e..cd2857e 100644
--- a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta
diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png
index 653a99e..653a99e 100644
--- a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png
Binary files differ
diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta
index a29299d..a29299d 100644
--- a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta
diff --git a/src/main/resources/resourcepacks/transparent_storage/pack.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/pack.mcmeta
index c37df06..035feaa 100644
--- a/src/main/resources/resourcepacks/transparent_storage/pack.mcmeta
+++ b/src/main/resources/resourcepacks/transparent_overlay/pack.mcmeta
@@ -5,6 +5,6 @@
"min_inclusive": 15,
"max_inclusive": 2147483647
},
- "description": "Adds a more transparent storage overlay for /firm storage"
+ "description": "Adds a more transparent overlay for Firmament"
}
}
diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png
deleted file mode 100644
index d4852d8..0000000
--- a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png
+++ /dev/null
Binary files differ