Split server and client modules and rename project to Saltatory
This commit is contained in:
parent
da43bf966b
commit
eccb914dcb
81 changed files with 160 additions and 146 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -1,6 +1,6 @@
|
|||
/.gradle/
|
||||
/build/
|
||||
/runs/
|
||||
**/.gradle/
|
||||
**/build/
|
||||
**/runs/
|
||||
/.idea/
|
||||
*.iml
|
||||
/docs/journal
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
# License
|
||||
|
||||
mc-tune is licensed under the GNU Lesser General Public License, version 3 only (`LGPL-3.0-only`). Copyright © 2026 mc-tune contributors.
|
||||
saltatory is licensed under the GNU Lesser General Public License, version 3 only (`LGPL-3.0-only`). Copyright © 2026 saltatory contributors.
|
||||
|
||||
The license consists of the additional permissions in [COPYING.LESSER](COPYING.LESSER) together with the GNU General Public License, version 3, in [COPYING](COPYING). Upstream attribution and modification notices appear in [NOTICE.md](NOTICE.md).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Third-party notices
|
||||
|
||||
| Upstream | License | Contribution to mc-tune |
|
||||
| Upstream | License | Contribution to saltatory |
|
||||
| ----------------------------------------------------------------------------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [Almanac 1.5.2](https://modrinth.com/mod/almanac), by frikinjay | LGPL-3.0-only | `AlmanacMixin` adapts `ItemNBTUtil` behavior from the released JAR's bytecode. Changes dated 2026-10-09 remove NBT copies and bypass normalization callbacks when custom data cannot change. |
|
||||
| [Mekanism 10.7.19.85](https://github.com/mekanism/Mekanism/tree/a00109e4856fd38b9c5b3dd7f22ce4a59cd65a80) | MIT | `SlotMemo`, `FlowBatch`, `InvMixin`, and `MekMixin` adapt `TransporterManager` inventory prediction. Changes dated 2026-10-09 memoize slot validation within one prediction snapshot and eliminate a repeated stack compatibility check. |
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
# mc-tune
|
||||
# Saltatory
|
||||
|
||||
A small server-side modmod that reduces inventory, crafting and NBT overhead in a personal modded Minecraft server.
|
||||
Small modmods for personal modded Minecraft server and client tuning.
|
||||
|
||||
Targets Minecraft 1.21.1 on GraalVM CE 25. Local builds require JDK 25 and Gradle 9.1.0.
|
||||
|
||||
```sh
|
||||
gradle --no-daemon format build
|
||||
gradle --no-daemon :server:format :server:build
|
||||
gradle --no-daemon :client:format :client:build
|
||||
```
|
||||
|
||||
[License](LICENSE.md) · [Third-party notices](NOTICE.md)
|
||||
|
|
|
|||
107
build.gradle
107
build.gradle
|
|
@ -1,74 +1,35 @@
|
|||
plugins {
|
||||
id 'java-library'
|
||||
id 'checkstyle'
|
||||
id 'net.neoforged.gradle.userdev' version '7.1.39'
|
||||
id 'base'
|
||||
id 'net.neoforged.gradle.userdev' version '7.1.39' apply false
|
||||
}
|
||||
|
||||
group = 'dev.quorate'
|
||||
version = '0.42.1'
|
||||
def modVersion = version.toString()
|
||||
subprojects { mod ->
|
||||
apply plugin: 'java-library'
|
||||
apply plugin: 'checkstyle'
|
||||
apply plugin: 'net.neoforged.gradle.userdev'
|
||||
|
||||
group = 'dev.quorate'
|
||||
java {
|
||||
toolchain.languageVersion = JavaLanguageVersion.of(25)
|
||||
withSourcesJar()
|
||||
}
|
||||
|
||||
repositories {
|
||||
ivy {
|
||||
url = 'https://github.com/tlaplus/tlaplus/releases/download'
|
||||
patternLayout { artifact '[revision]/[artifact].[ext]' }
|
||||
metadataSources { artifact() }
|
||||
content { includeModule 'org.lamport', 'tla2tools' }
|
||||
}
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = 'https://maven.blamejared.com/'
|
||||
content { includeGroup 'com.hrznstudio' }
|
||||
}
|
||||
maven {
|
||||
url = 'https://modmaven.dev/'
|
||||
content { includeGroup 'mekanism' }
|
||||
}
|
||||
maven {
|
||||
url = 'https://www.cursemaven.com'
|
||||
content { includeGroup 'curse.maven' }
|
||||
}
|
||||
maven {
|
||||
url = 'https://api.modrinth.com/maven'
|
||||
content { includeGroup 'maven.modrinth' }
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
probe {
|
||||
compileClasspath += sourceSets.main.output + configurations.compileClasspath
|
||||
}
|
||||
}
|
||||
repositories { mavenCentral() }
|
||||
configurations { javafmt }
|
||||
dependencies {
|
||||
implementation 'net.neoforged:neoforge:21.1.250'
|
||||
compileOnly('mekanism:Mekanism:1.21.1-10.7.19.85') { transitive = false }
|
||||
compileOnly('curse.maven:functional-storage-556861:8459097') { transitive = false }
|
||||
compileOnly('maven.modrinth:refined-storage:lHHiI26k') { transitive = false }
|
||||
compileOnly 'org.jetbrains:annotations:24.1.0'
|
||||
compileOnly 'org.apiguardian:apiguardian-api:1.1.2'
|
||||
compileOnly 'io.github.llamalad7:mixinextras-neoforge:0.5.3'
|
||||
probeCompileOnly('com.hrznstudio:titanium:1.21-4.0.43') { transitive = false }
|
||||
testImplementation 'it.unimi.dsi:fastutil:8.5.12'
|
||||
testImplementation('org.lamport:tla2tools:v1.7.4') { transitive = false }
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.11.4'
|
||||
javafmt('com.google.googlejavaformat:google-java-format:1.37.0:all-deps') { transitive = false }
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
toolVersion = '14.3.0'
|
||||
configFile = file('quality.xml')
|
||||
configFile = rootProject.file('quality.xml')
|
||||
}
|
||||
|
||||
def javaSources = fileTree('src') { include '**/*.java' }
|
||||
['format': ['--replace'], 'formatCheck': ['--dry-run', '--set-exit-if-changed']].each { name, mode ->
|
||||
tasks.register(name, JavaExec) {
|
||||
inputs.files(javaSources).skipWhenEmpty()
|
||||
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) }
|
||||
classpath = configurations.javafmt
|
||||
mainClass = 'com.google.googlejavaformat.java.Main'
|
||||
|
|
@ -80,61 +41,33 @@ def javaSources = fileTree('src') { include '**/*.java' }
|
|||
}
|
||||
tasks.named('formatCheck') { mustRunAfter 'format' }
|
||||
tasks.withType(Checkstyle).configureEach { mustRunAfter 'format' }
|
||||
tasks.register('quality') { dependsOn 'formatCheck', 'checkstyleMain', 'checkstyleTest', 'checkstyleProbe' }
|
||||
tasks.register('quality') { dependsOn 'formatCheck', 'checkstyleMain', 'checkstyleTest' }
|
||||
tasks.named('check') { dependsOn 'quality' }
|
||||
tasks.named('checkstyleProbe') {
|
||||
dependsOn 'compileProbeJava'
|
||||
classpath = sourceSets.probe.compileClasspath
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
dependsOn 'quality'
|
||||
inputs.dir('src/test/tla')
|
||||
useJUnitPlatform()
|
||||
maxHeapSize = '512m'
|
||||
jvmArgs '-XX:+UseZGC'
|
||||
}
|
||||
|
||||
tasks.named('processResources') {
|
||||
inputs.property 'version', modVersion
|
||||
filesMatching('META-INF/neoforge.mods.toml') { expand version: modVersion }
|
||||
inputs.property 'version', provider { mod.version.toString() }
|
||||
filesMatching('META-INF/neoforge.mods.toml') { expand version: mod.version.toString() }
|
||||
}
|
||||
|
||||
['compileJava', 'compileTestJava', 'compileProbeJava'].each { name ->
|
||||
tasks.named(name, JavaCompile) {
|
||||
tasks.withType(JavaCompile).matching { it.name in ['compileJava', 'compileTestJava', 'compileProbeJava'] }.configureEach {
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs += ['-Xlint:all', '-Werror', '-proc:none']
|
||||
mustRunAfter 'format'
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Jar).configureEach {
|
||||
preserveFileTimestamps = false
|
||||
reproducibleFileOrder = true
|
||||
from(['LICENSE.md', 'COPYING', 'COPYING.LESSER', 'NOTICE.md']) { into 'META-INF' }
|
||||
}
|
||||
|
||||
tasks.named('processProbeResources') {
|
||||
inputs.property 'version', modVersion
|
||||
filesMatching('META-INF/neoforge.mods.toml') { expand version: modVersion }
|
||||
dependsOn 'test'
|
||||
['Slots', 'SlotOverflow', 'Traits'].each { name ->
|
||||
from(layout.buildDirectory.dir("reports/models/${name}")) {
|
||||
include 'cases.jsonl', 'stats.json'
|
||||
eachFile { entry -> entry.path = "cases/${name}." + (entry.name == 'cases.jsonl' ? 'jsonl' : 'json') }
|
||||
}
|
||||
}
|
||||
from({ zipTree(configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts.find {
|
||||
it.moduleVersion.id.group == 'mekanism' && it.name == 'Mekanism'
|
||||
}.file) }) {
|
||||
include 'mekanism/common/content/transporter/TransporterManager*.class'
|
||||
into 'baseline'
|
||||
from(['LICENSE.md', 'COPYING', 'COPYING.LESSER', 'NOTICE.md'].collect { rootProject.file(it) }) { into 'META-INF' }
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('probeJar', Jar) {
|
||||
dependsOn 'quality', 'probeClasses'
|
||||
archiveBaseName = 'mc-checks'
|
||||
destinationDirectory = layout.buildDirectory.dir('probe')
|
||||
from sourceSets.probe.output
|
||||
['format', 'formatCheck', 'quality'].each { name ->
|
||||
tasks.register(name) { dependsOn subprojects.collect { it.tasks.named(name) } }
|
||||
}
|
||||
['assemble', 'check'].each { name ->
|
||||
tasks.named(name) { dependsOn subprojects.collect { it.tasks.named(name) } }
|
||||
}
|
||||
|
|
|
|||
2
client/build.gradle
Normal file
2
client/build.gradle
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
version = '0.1.0'
|
||||
base.archivesName = 'saltatory-client'
|
||||
77
server/build.gradle
Normal file
77
server/build.gradle
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
version = '0.42.1'
|
||||
base.archivesName = 'saltatory'
|
||||
|
||||
repositories {
|
||||
ivy {
|
||||
url = 'https://github.com/tlaplus/tlaplus/releases/download'
|
||||
patternLayout { artifact '[revision]/[artifact].[ext]' }
|
||||
metadataSources { artifact() }
|
||||
content { includeModule 'org.lamport', 'tla2tools' }
|
||||
}
|
||||
maven {
|
||||
url = 'https://maven.blamejared.com/'
|
||||
content { includeGroup 'com.hrznstudio' }
|
||||
}
|
||||
maven {
|
||||
url = 'https://modmaven.dev/'
|
||||
content { includeGroup 'mekanism' }
|
||||
}
|
||||
maven {
|
||||
url = 'https://www.cursemaven.com'
|
||||
content { includeGroup 'curse.maven' }
|
||||
}
|
||||
maven {
|
||||
url = 'https://api.modrinth.com/maven'
|
||||
content { includeGroup 'maven.modrinth' }
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
probe {
|
||||
compileClasspath += sourceSets.main.output + configurations.compileClasspath
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly('mekanism:Mekanism:1.21.1-10.7.19.85') { transitive = false }
|
||||
compileOnly('curse.maven:functional-storage-556861:8459097') { transitive = false }
|
||||
compileOnly('maven.modrinth:refined-storage:lHHiI26k') { transitive = false }
|
||||
compileOnly 'org.jetbrains:annotations:24.1.0'
|
||||
compileOnly 'org.apiguardian:apiguardian-api:1.1.2'
|
||||
compileOnly 'io.github.llamalad7:mixinextras-neoforge:0.5.3'
|
||||
probeCompileOnly('com.hrznstudio:titanium:1.21-4.0.43') { transitive = false }
|
||||
testImplementation 'it.unimi.dsi:fastutil:8.5.12'
|
||||
testImplementation('org.lamport:tla2tools:v1.7.4') { transitive = false }
|
||||
}
|
||||
|
||||
tasks.named('quality') { dependsOn 'checkstyleProbe' }
|
||||
tasks.named('checkstyleProbe') {
|
||||
dependsOn 'compileProbeJava'
|
||||
classpath = sourceSets.probe.compileClasspath
|
||||
}
|
||||
tasks.named('test') { inputs.dir('src/test/tla') }
|
||||
|
||||
tasks.named('processProbeResources') {
|
||||
inputs.property 'version', provider { project.version.toString() }
|
||||
filesMatching('META-INF/neoforge.mods.toml') { expand version: project.version.toString() }
|
||||
dependsOn 'test'
|
||||
['Slots', 'SlotOverflow', 'Traits'].each { name ->
|
||||
from(layout.buildDirectory.dir("reports/models/${name}")) {
|
||||
include 'cases.jsonl', 'stats.json'
|
||||
eachFile { entry -> entry.path = "cases/${name}." + (entry.name == 'cases.jsonl' ? 'jsonl' : 'json') }
|
||||
}
|
||||
}
|
||||
from({ zipTree(configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts.find {
|
||||
it.moduleVersion.id.group == 'mekanism' && it.name == 'Mekanism'
|
||||
}.file) }) {
|
||||
include 'mekanism/common/content/transporter/TransporterManager*.class'
|
||||
into 'baseline'
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register('probeJar', Jar) {
|
||||
dependsOn 'quality', 'probeClasses'
|
||||
archiveBaseName = 'saltatory-checks'
|
||||
destinationDirectory = layout.buildDirectory.dir('probe')
|
||||
from sourceSets.probe.output
|
||||
}
|
||||
29
server/src/main/java/dev/quorate/tune/ComponentMap.java
Normal file
29
server/src/main/java/dev/quorate/tune/ComponentMap.java
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import it.unimi.dsi.fastutil.HashCommon;
|
||||
import it.unimi.dsi.fastutil.objects.Reference2ObjectLinkedOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
|
||||
|
||||
public final class ComponentMap<K, V> extends Reference2ObjectLinkedOpenHashMap<K, V> {
|
||||
private static final long serialVersionUID = 1L;
|
||||
// Deserialized maps use the original lookup because key identities change.
|
||||
private final transient K hot;
|
||||
private final transient int hash;
|
||||
|
||||
public ComponentMap(Reference2ObjectMap<K, V> map, K hot) {
|
||||
super(map);
|
||||
this.hot = hot;
|
||||
hash = HashCommon.mix(System.identityHashCode(hot));
|
||||
}
|
||||
|
||||
@Override
|
||||
public V get(Object component) {
|
||||
if (component != null && component == hot) {
|
||||
int slot = hash & mask;
|
||||
var stored = key[slot];
|
||||
if (stored == component) return value[slot];
|
||||
if (stored == null) return defRetValue;
|
||||
}
|
||||
return super.get(component);
|
||||
}
|
||||
}
|
||||
154
server/src/main/java/dev/quorate/tune/FlowBatch.java
Normal file
154
server/src/main/java/dev/quorate/tune/FlowBatch.java
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import com.buuz135.functionalstorage.inventory.ControllerInventoryHandler;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
|
||||
|
||||
import mekanism.common.content.transporter.TransporterStack;
|
||||
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.neoforged.neoforge.items.IItemHandler;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Set;
|
||||
|
||||
public final class FlowBatch {
|
||||
private static final class State {
|
||||
private final IItemHandler handler;
|
||||
private final ItemStack[] inventory;
|
||||
private final int[] sizes;
|
||||
private final int[] limits;
|
||||
private final boolean[] valid;
|
||||
private final boolean[] certified;
|
||||
private final int[] empty;
|
||||
private final int emptyCount;
|
||||
private final Reference2IntOpenHashMap<Item> first;
|
||||
private Reference2ObjectOpenHashMap<Item, long[]> denied;
|
||||
|
||||
private State(IItemHandler handler, ItemStack[] inventory, int[] sizes) {
|
||||
this.handler = handler;
|
||||
this.inventory = inventory.clone();
|
||||
this.sizes = sizes.clone();
|
||||
limits = new int[sizes.length];
|
||||
Arrays.fill(limits, -1);
|
||||
valid = new boolean[sizes.length];
|
||||
certified = new boolean[sizes.length];
|
||||
empty = new int[sizes.length];
|
||||
first = new Reference2IntOpenHashMap<>(sizes.length);
|
||||
first.defaultReturnValue(-1);
|
||||
int count = 0;
|
||||
for (int slot = 0; slot < sizes.length; slot++) {
|
||||
if (sizes[slot] == 0) empty[count++] = slot;
|
||||
else first.putIfAbsent(inventory[slot].getItem(), slot);
|
||||
}
|
||||
emptyCount = count;
|
||||
}
|
||||
|
||||
private int limit(int slot) {
|
||||
if (limits[slot] == -1) limits[slot] = handler.getSlotLimit(slot);
|
||||
return limits[slot];
|
||||
}
|
||||
|
||||
private boolean rejected(int ordinal, Item item, int count) {
|
||||
if (denied == null) return false;
|
||||
var masks = denied.get(item);
|
||||
return masks != null && (masks[ordinal] & 1L << (count - 1)) != 0;
|
||||
}
|
||||
|
||||
private void reject(int ordinal, Item item, int count) {
|
||||
if (denied == null) denied = new Reference2ObjectOpenHashMap<>();
|
||||
denied.computeIfAbsent(item, _ -> new long[emptyCount])[ordinal] |= 1L << (count - 1);
|
||||
}
|
||||
|
||||
private boolean insert(ItemStack stack, int count) {
|
||||
var item = stack.getItem();
|
||||
int target = first.getInt(item);
|
||||
for (int ordinal = 0; ordinal < emptyCount; ordinal++) {
|
||||
int slot = empty[ordinal];
|
||||
if (target >= 0 && slot >= target) break;
|
||||
if (sizes[slot] > 0 || rejected(ordinal, item, count)) continue;
|
||||
int limit = limit(slot);
|
||||
if (limit == 0) continue;
|
||||
if (limit < 0) return false;
|
||||
if (!handler.isItemValid(slot, stack)) {
|
||||
reject(ordinal, item, count);
|
||||
continue;
|
||||
}
|
||||
var remainder = handler.insertItem(slot, stack, true);
|
||||
if (remainder == ItemStack.EMPTY) {
|
||||
if (count > limit) return false;
|
||||
inventory[slot] = stack;
|
||||
sizes[slot] = count;
|
||||
first.put(item, slot);
|
||||
valid[slot] = true;
|
||||
return true;
|
||||
}
|
||||
if (remainder.getCount() == count) {
|
||||
reject(ordinal, item, count);
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (target < 0) return false;
|
||||
if (!plain(inventory[target])) return false;
|
||||
if (!valid[target]) {
|
||||
if (!handler.isItemValid(target, stack)) return false;
|
||||
valid[target] = true;
|
||||
}
|
||||
int limit = limit(target);
|
||||
long merged = (long) sizes[target] + count;
|
||||
if (limit <= 0) return false;
|
||||
if (merged > limit) return false;
|
||||
if (limit > 64 && merged > 64 && !certified[target]) {
|
||||
if (handler.insertItem(target, stack.copyWithCount(65), true) != ItemStack.EMPTY)
|
||||
return false;
|
||||
certified[target] = true;
|
||||
}
|
||||
sizes[target] = (int) merged;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void commit(ItemStack[] target, int[] counts) {
|
||||
System.arraycopy(inventory, 0, target, 0, target.length);
|
||||
System.arraycopy(sizes, 0, counts, 0, counts.length);
|
||||
}
|
||||
}
|
||||
|
||||
private FlowBatch() {}
|
||||
|
||||
private static boolean known(IItemHandler handler) {
|
||||
if (handler instanceof ControllerInventoryHandler controller)
|
||||
return handler.getClass()
|
||||
.getName()
|
||||
.equals(
|
||||
"com.buuz135.functionalstorage.block.tile.StorageControllerTile$1")
|
||||
&& controller.getDrawers().getItemHandlers().stream().allMatch(Handlers::leaf);
|
||||
return Handlers.leaf(handler);
|
||||
}
|
||||
|
||||
private static boolean plain(ItemStack stack) {
|
||||
return stack.getComponentsPatch() == DataComponentPatch.EMPTY
|
||||
&& stack.get(DataComponents.CUSTOM_DATA) == null;
|
||||
}
|
||||
|
||||
public static boolean apply(
|
||||
IItemHandler handler, ItemStack[] inventory, int[] sizes, Set<TransporterStack> flows) {
|
||||
if (!known(handler)) return false;
|
||||
for (int size : sizes) if (size < 0) return false;
|
||||
var state = new State(handler, inventory, sizes);
|
||||
for (var flow : flows) {
|
||||
if (flow == null || !flow.getPathType().hasTarget()) continue;
|
||||
var stack = flow.itemStack;
|
||||
if (!plain(stack)) return false;
|
||||
int count = stack.getCount();
|
||||
if (count <= 0 || count > 64 || stack.getMaxStackSize() != 64) return false;
|
||||
if (!state.insert(stack, count)) return false;
|
||||
}
|
||||
state.commit(inventory, sizes);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
32
server/src/main/java/dev/quorate/tune/Handlers.java
Normal file
32
server/src/main/java/dev/quorate/tune/Handlers.java
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import com.buuz135.functionalstorage.inventory.ControllerInventoryHandler;
|
||||
|
||||
import net.neoforged.neoforge.items.IItemHandler;
|
||||
|
||||
public final class Handlers {
|
||||
private Handlers() {}
|
||||
|
||||
public static boolean known(IItemHandler handler) {
|
||||
if (handler instanceof ControllerInventoryHandler controller) {
|
||||
if (!handler.getClass()
|
||||
.getName()
|
||||
.equals("com.buuz135.functionalstorage.block.tile.StorageControllerTile$1"))
|
||||
return false;
|
||||
return controller.getDrawers().getItemHandlers().stream()
|
||||
.allMatch(
|
||||
member -> member instanceof ControllerInventoryHandler || leaf(member));
|
||||
}
|
||||
return leaf(handler);
|
||||
}
|
||||
|
||||
static boolean leaf(IItemHandler handler) {
|
||||
return switch (handler.getClass().getName()) {
|
||||
case "com.buuz135.functionalstorage.block.tile.DrawerTile$1",
|
||||
"com.buuz135.functionalstorage.block.tile.CompactingDrawerTile$1",
|
||||
"com.buuz135.functionalstorage.block.tile.SimpleCompactingDrawerTile$1" ->
|
||||
true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
}
|
||||
71
server/src/main/java/dev/quorate/tune/MemberList.java
Normal file
71
server/src/main/java/dev/quorate/tune/MemberList.java
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import java.util.AbstractList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.RandomAccess;
|
||||
|
||||
public final class MemberList<E> extends AbstractList<E> implements RandomAccess {
|
||||
private final ArrayList<E> entries;
|
||||
private final Map<E, Integer> counts = new HashMap<>();
|
||||
|
||||
public MemberList(Collection<? extends E> entries) {
|
||||
this.entries = new ArrayList<>(entries);
|
||||
for (var entry : entries) addCount(entry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public E get(int index) {
|
||||
return entries.get(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object value) {
|
||||
return counts.containsKey(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void add(int index, E value) {
|
||||
entries.add(index, value);
|
||||
addCount(value);
|
||||
modCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public E set(int index, E value) {
|
||||
E old = entries.set(index, value);
|
||||
removeCount(old);
|
||||
addCount(value);
|
||||
return old;
|
||||
}
|
||||
|
||||
@Override
|
||||
public E remove(int index) {
|
||||
E old = entries.remove(index);
|
||||
removeCount(old);
|
||||
modCount++;
|
||||
return old;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
entries.clear();
|
||||
counts.clear();
|
||||
modCount++;
|
||||
}
|
||||
|
||||
private void addCount(E value) {
|
||||
counts.merge(value, 1, Integer::sum);
|
||||
}
|
||||
|
||||
private void removeCount(E value) {
|
||||
counts.compute(value, (key, count) -> count == 1 ? null : count - 1);
|
||||
}
|
||||
}
|
||||
82
server/src/main/java/dev/quorate/tune/NbtTree.java
Normal file
82
server/src/main/java/dev/quorate/tune/NbtTree.java
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import net.minecraft.nbt.ByteArrayTag;
|
||||
import net.minecraft.nbt.ByteTag;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.DoubleTag;
|
||||
import net.minecraft.nbt.EndTag;
|
||||
import net.minecraft.nbt.FloatTag;
|
||||
import net.minecraft.nbt.IntArrayTag;
|
||||
import net.minecraft.nbt.IntTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
import net.minecraft.nbt.LongArrayTag;
|
||||
import net.minecraft.nbt.LongTag;
|
||||
import net.minecraft.nbt.NbtOps;
|
||||
import net.minecraft.nbt.ShortTag;
|
||||
import net.minecraft.nbt.StringTag;
|
||||
import net.minecraft.nbt.Tag;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public final class NbtTree {
|
||||
private NbtTree() {}
|
||||
|
||||
public static Tag convert(Tag input) {
|
||||
if (input == null) return null;
|
||||
// Tag subclasses may override copying; their encoding stays with the native codec.
|
||||
var type = input.getClass();
|
||||
return switch (input) {
|
||||
case CompoundTag compound when type == CompoundTag.class -> {
|
||||
var result = new CompoundTag();
|
||||
for (var name : compound.getAllKeys()) {
|
||||
if (name == null) yield null;
|
||||
var value = convert(compound.get(name));
|
||||
if (value == null) yield null;
|
||||
result.put(name, value);
|
||||
}
|
||||
yield result;
|
||||
}
|
||||
case ListTag list when type == ListTag.class -> {
|
||||
var entries = new ArrayList<Tag>(list.size());
|
||||
for (var element : list) {
|
||||
var value = element;
|
||||
if (list.getElementType() == Tag.TAG_COMPOUND) {
|
||||
if (element == null || element.getClass() != CompoundTag.class) yield null;
|
||||
var compound = (CompoundTag) element;
|
||||
if (compound.size() == 1 && compound.get("") != null)
|
||||
value = compound.get("");
|
||||
}
|
||||
var converted = convert(value);
|
||||
if (converted == null) yield null;
|
||||
entries.add(converted);
|
||||
}
|
||||
yield NbtOps.INSTANCE.createList(entries.stream());
|
||||
}
|
||||
case ByteArrayTag array when type == ByteArrayTag.class -> {
|
||||
var data = array.getAsByteArray();
|
||||
yield data == null ? null : new ByteArrayTag(data.clone());
|
||||
}
|
||||
case IntArrayTag array when type == IntArrayTag.class -> {
|
||||
var data = array.getAsIntArray();
|
||||
yield data == null ? null : new IntArrayTag(data.clone());
|
||||
}
|
||||
case LongArrayTag array when type == LongArrayTag.class -> {
|
||||
var data = array.getAsLongArray();
|
||||
yield data == null ? null : new LongArrayTag(data.clone());
|
||||
}
|
||||
case ByteTag number when type == ByteTag.class -> ByteTag.valueOf(number.getAsByte());
|
||||
case ShortTag number when type == ShortTag.class ->
|
||||
ShortTag.valueOf(number.getAsShort());
|
||||
case IntTag number when type == IntTag.class -> IntTag.valueOf(number.getAsInt());
|
||||
case LongTag number when type == LongTag.class -> LongTag.valueOf(number.getAsLong());
|
||||
case FloatTag number when type == FloatTag.class ->
|
||||
FloatTag.valueOf(number.getAsFloat());
|
||||
case DoubleTag number when type == DoubleTag.class ->
|
||||
DoubleTag.valueOf(number.getAsDouble());
|
||||
case StringTag string when type == StringTag.class ->
|
||||
StringTag.valueOf(string.getAsString());
|
||||
case EndTag _ when type == EndTag.class -> EndTag.INSTANCE;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
20
server/src/main/java/dev/quorate/tune/PlanRows.java
Normal file
20
server/src/main/java/dev/quorate/tune/PlanRows.java
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class PlanRows<K> {
|
||||
private boolean shared;
|
||||
|
||||
public void share() {
|
||||
shared = true;
|
||||
}
|
||||
|
||||
public void prepare(Map<Integer, Map<K, Long>> rows) {
|
||||
if (!shared) return;
|
||||
for (var entry : rows.entrySet()) {
|
||||
entry.setValue(new LinkedHashMap<>(entry.getValue()));
|
||||
}
|
||||
shared = false;
|
||||
}
|
||||
}
|
||||
5
server/src/main/java/dev/quorate/tune/PlanShare.java
Normal file
5
server/src/main/java/dev/quorate/tune/PlanShare.java
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
public interface PlanShare {
|
||||
void mctune$share();
|
||||
}
|
||||
24
server/src/main/java/dev/quorate/tune/Prediction.java
Normal file
24
server/src/main/java/dev/quorate/tune/Prediction.java
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import mekanism.common.content.transporter.TransporterStack;
|
||||
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.neoforged.neoforge.items.IItemHandler;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public interface Prediction {
|
||||
boolean mctune$batch(IItemHandler handler, Set<TransporterStack> flows);
|
||||
|
||||
boolean mctune$check(IItemHandler handler, int slot, ItemStack stack, boolean inFlight);
|
||||
|
||||
boolean mctune$matches(ItemStack left, ItemStack right);
|
||||
|
||||
int mctune$next(int slot, ItemStack stack, int count);
|
||||
|
||||
boolean mctune$begin(ItemStack stack, int count, boolean inFlight);
|
||||
|
||||
void mctune$finish(ItemStack stack, int count, boolean inFlight, int left);
|
||||
|
||||
ItemStack mctune$simulate(IItemHandler handler, int slot, ItemStack stack, boolean simulate);
|
||||
}
|
||||
54
server/src/main/java/dev/quorate/tune/SimCache.java
Normal file
54
server/src/main/java/dev/quorate/tune/SimCache.java
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class SimCache<K> {
|
||||
private record Result<K>(K item, int count, int left) {}
|
||||
|
||||
private final Object[] items;
|
||||
private final int[] counts;
|
||||
private final int[] remaining;
|
||||
private final List<List<Result<K>>> extra;
|
||||
|
||||
public SimCache(int slots) {
|
||||
items = new Object[slots];
|
||||
counts = new int[slots];
|
||||
remaining = new int[slots];
|
||||
extra = new ArrayList<>(Collections.nCopies(slots, null));
|
||||
}
|
||||
|
||||
public int get(int slot, K item, int count) {
|
||||
if (items[slot] == item && counts[slot] == count) return remaining[slot];
|
||||
var results = extra.get(slot);
|
||||
if (results != null)
|
||||
for (var result : results) {
|
||||
if (result.item() == item && result.count() == count) return result.left();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void put(int slot, K item, int count, int left) {
|
||||
if (items[slot] == null || items[slot] == item && counts[slot] == count) {
|
||||
items[slot] = item;
|
||||
counts[slot] = count;
|
||||
remaining[slot] = left;
|
||||
return;
|
||||
}
|
||||
var results = extra.get(slot);
|
||||
if (results == null) {
|
||||
results = new ArrayList<>();
|
||||
extra.set(slot, results);
|
||||
}
|
||||
var entry = new Result<>(item, count, left);
|
||||
for (int index = 0; index < results.size(); index++) {
|
||||
var result = results.get(index);
|
||||
if (result.item() == item && result.count() == count) {
|
||||
results.set(index, entry);
|
||||
return;
|
||||
}
|
||||
}
|
||||
results.add(entry);
|
||||
}
|
||||
}
|
||||
310
server/src/main/java/dev/quorate/tune/SlotMemo.java
Normal file
310
server/src/main/java/dev/quorate/tune/SlotMemo.java
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import mekanism.common.util.InventoryUtils;
|
||||
|
||||
import net.minecraft.core.component.DataComponentPatch;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.neoforged.neoforge.items.IItemHandler;
|
||||
|
||||
import java.util.BitSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class SlotMemo {
|
||||
private static final int VALID = 1;
|
||||
private static final int INVALID = 2;
|
||||
private static final int MATCH = 4;
|
||||
private static final int DIFFERENT = 8;
|
||||
|
||||
private record StackKey(Item item, DataComponentPatch data, boolean empty) {}
|
||||
|
||||
private record AppendRule(int slot, int limit, int maxStack, ItemStack stack) {}
|
||||
|
||||
private static final class CountState {
|
||||
private final byte[] valid;
|
||||
private final BitSet slots;
|
||||
private AppendRule line;
|
||||
|
||||
private CountState(int size, BitSet slots) {
|
||||
valid = new byte[size];
|
||||
this.slots = slots;
|
||||
}
|
||||
}
|
||||
|
||||
private record StackState(
|
||||
StackKey key, byte[] matches, BitSet slots, Map<Integer, CountState> rows) {}
|
||||
|
||||
// InventoryInfo confines cached decisions to one TransporterManager prediction snapshot.
|
||||
private final ItemStack[] inventory;
|
||||
private final int[] sizes;
|
||||
private final int[] limits;
|
||||
private final int[] actual;
|
||||
private final SimCache<Item> trials;
|
||||
private final Item[] kinds;
|
||||
private final boolean[] compared;
|
||||
private final BitSet eligible;
|
||||
private final BitSet universal;
|
||||
private final Map<Item, BitSet> indices;
|
||||
private final Map<StackKey, StackState> types = new HashMap<>();
|
||||
private int offered = -1;
|
||||
private ItemStack before;
|
||||
private int beforeSize;
|
||||
private int capacity;
|
||||
private boolean stable;
|
||||
private boolean changed;
|
||||
private StackState type;
|
||||
private CountState row;
|
||||
private ItemStack selected;
|
||||
private int selectedCount;
|
||||
|
||||
public SlotMemo(
|
||||
ItemStack[] inventory, int[] sizes, int[] actual, int[] limits, boolean inFlight) {
|
||||
indices = inFlight ? new IdentityHashMap<>() : null;
|
||||
this.inventory = inventory;
|
||||
this.sizes = sizes;
|
||||
this.limits = limits;
|
||||
this.actual = actual;
|
||||
trials = new SimCache<>(inventory.length);
|
||||
kinds = new Item[inventory.length];
|
||||
compared = new boolean[inventory.length];
|
||||
eligible = new BitSet(inventory.length);
|
||||
eligible.set(0, inventory.length);
|
||||
universal = new BitSet(inventory.length);
|
||||
for (int slot = 0; slot < inventory.length; slot++) {
|
||||
var item = sizes[slot] > 0 && actual[slot] > 0 ? inventory[slot].getItem() : null;
|
||||
if (item == Items.AIR) item = null;
|
||||
kinds[slot] = item;
|
||||
if (item == null) universal.set(slot);
|
||||
else if (indices != null) index(item).set(slot);
|
||||
}
|
||||
}
|
||||
|
||||
private BitSet index(Item item) {
|
||||
var slots = indices.get(item);
|
||||
if (slots == null) {
|
||||
slots = new BitSet(inventory.length);
|
||||
indices.put(item, slots);
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
|
||||
private StackState create(StackKey key) {
|
||||
var slots = key.empty() ? (BitSet) eligible.clone() : (BitSet) universal.clone();
|
||||
if (!key.empty()) {
|
||||
if (indices == null) {
|
||||
for (int slot = 0; slot < kinds.length; slot++)
|
||||
if (kinds[slot] == key.item()) slots.set(slot);
|
||||
} else {
|
||||
var matching = indices.get(key.item());
|
||||
if (matching != null) slots.or(matching);
|
||||
}
|
||||
}
|
||||
slots.and(eligible);
|
||||
return new StackState(key, new byte[inventory.length], slots, HashMap.newHashMap(2));
|
||||
}
|
||||
|
||||
private void select(ItemStack stack) {
|
||||
if (selected == stack) return;
|
||||
int count = stack.getCount();
|
||||
var item = stack.getItem();
|
||||
var data = stack.getComponentsPatch();
|
||||
boolean empty = count <= 0 || item == Items.AIR;
|
||||
boolean changed =
|
||||
data != DataComponentPatch.EMPTY
|
||||
|| type == null
|
||||
|| type.key().data() != data
|
||||
|| type.key().item() != item
|
||||
|| type.key().empty() != empty;
|
||||
if (changed) {
|
||||
var key = new StackKey(item, data, empty);
|
||||
type = types.get(key);
|
||||
if (type == null) {
|
||||
type = create(key);
|
||||
types.put(key, type);
|
||||
}
|
||||
}
|
||||
if (changed || selectedCount != count) {
|
||||
row = type.rows().get(count);
|
||||
if (row == null) {
|
||||
row = new CountState(inventory.length, (BitSet) type.slots().clone());
|
||||
type.rows().put(count, row);
|
||||
}
|
||||
}
|
||||
selectedCount = count;
|
||||
selected = stack;
|
||||
}
|
||||
|
||||
public boolean check(IItemHandler handler, int slot, ItemStack stack, int limit) {
|
||||
offer(slot);
|
||||
int size = sizes[slot];
|
||||
if (size > 0 && size >= limit) {
|
||||
if (limit != -1) exclude(slot);
|
||||
return false;
|
||||
}
|
||||
if (size <= 0 && compared[slot]) reset(slot);
|
||||
select(stack);
|
||||
if (!row.slots.get(slot)) return false;
|
||||
if (size > 0) {
|
||||
int state = type.matches()[slot];
|
||||
if (state == 0) {
|
||||
state =
|
||||
InventoryUtils.areItemsStackable(inventory[slot], stack)
|
||||
? MATCH
|
||||
: DIFFERENT;
|
||||
type.matches()[slot] = (byte) state;
|
||||
compared[slot] = true;
|
||||
}
|
||||
if (state == DIFFERENT) {
|
||||
type.slots().clear(slot);
|
||||
for (var value : type.rows().values()) value.slots.clear(slot);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
int valid = row.valid[slot];
|
||||
if (valid == 0) {
|
||||
valid = handler.isItemValid(slot, stack) ? VALID : INVALID;
|
||||
row.valid[slot] = (byte) valid;
|
||||
}
|
||||
if (valid == INVALID) row.slots.clear(slot);
|
||||
return valid == VALID;
|
||||
}
|
||||
|
||||
private void flush() {
|
||||
if (offered < 0) return;
|
||||
int slot = offered;
|
||||
offered = -1;
|
||||
changed |= sizes[slot] != beforeSize;
|
||||
int limit = limits[slot];
|
||||
if (limit != -1 && (limit == 0 || sizes[slot] > 0 && sizes[slot] >= limit)) {
|
||||
exclude(slot);
|
||||
}
|
||||
refresh(slot);
|
||||
}
|
||||
|
||||
public int next(int start, ItemStack stack, int count) {
|
||||
flush();
|
||||
if (count == 0 || start >= inventory.length) return start;
|
||||
if (selected != stack
|
||||
&& stack.getComponentsPatch() == DataComponentPatch.EMPTY
|
||||
&& stack.get(DataComponents.CUSTOM_DATA) == null) select(stack);
|
||||
var slots = selected == stack ? row.slots : eligible;
|
||||
int slot = slots.nextSetBit(start);
|
||||
if (slot >= 0) offer(slot);
|
||||
return slot < 0 ? inventory.length : slot;
|
||||
}
|
||||
|
||||
private static boolean plain(ItemStack stack) {
|
||||
return stack.getComponentsPatch() == DataComponentPatch.EMPTY
|
||||
&& stack.get(DataComponents.CUSTOM_DATA) == null;
|
||||
}
|
||||
|
||||
public boolean begin(ItemStack stack, int count, boolean inFlight) {
|
||||
changed = false;
|
||||
if (!inFlight || count <= 0 || !plain(stack)) return false;
|
||||
select(stack);
|
||||
var line = row.line;
|
||||
if (line == null
|
||||
|| selectedCount != count
|
||||
|| row.slots.nextSetBit(0) != line.slot()
|
||||
|| inventory[line.slot()] != line.stack()
|
||||
|| sizes[line.slot()] <= line.maxStack()
|
||||
|| (long) sizes[line.slot()] + count > line.limit()) return false;
|
||||
sizes[line.slot()] += count;
|
||||
if (sizes[line.slot()] >= limits[line.slot()]) exclude(line.slot());
|
||||
return true;
|
||||
}
|
||||
|
||||
public void finish(ItemStack stack, int count, boolean inFlight, int left) {
|
||||
if (inFlight
|
||||
&& left == 0
|
||||
&& !changed
|
||||
&& offered >= 0
|
||||
&& stable
|
||||
&& count > 0
|
||||
&& selected == stack
|
||||
&& selectedCount == count
|
||||
&& beforeSize > stack.getMaxStackSize()
|
||||
&& (long) sizes[offered] - beforeSize == count
|
||||
&& row.slots.nextSetBit(0) == offered) {
|
||||
row.line =
|
||||
new AppendRule(offered, capacity, stack.getMaxStackSize(), inventory[offered]);
|
||||
}
|
||||
flush();
|
||||
}
|
||||
|
||||
public ItemStack simulate(IItemHandler handler, int slot, ItemStack stack, boolean simulate) {
|
||||
if (!plain(stack)) return handler.insertItem(slot, stack, simulate);
|
||||
int count = stack.getCount();
|
||||
var item = stack.getItem();
|
||||
int left = trials.get(slot, item, count);
|
||||
ItemStack result;
|
||||
if (left < 0) {
|
||||
result = handler.insertItem(slot, stack, simulate);
|
||||
left = result.getCount();
|
||||
trials.put(slot, item, count, left);
|
||||
} else
|
||||
result =
|
||||
left == count ? stack : left == 0 ? ItemStack.EMPTY : stack.copyWithCount(left);
|
||||
int accepted = count - left;
|
||||
capacity =
|
||||
accepted < count ? Math.min(limits[slot], actual[slot] + accepted) : limits[slot];
|
||||
stable = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
private void offer(int slot) {
|
||||
offered = slot;
|
||||
before = inventory[slot];
|
||||
beforeSize = sizes[slot];
|
||||
stable = false;
|
||||
}
|
||||
|
||||
private void refresh(int slot) {
|
||||
var previous = kinds[slot];
|
||||
Item current = previous;
|
||||
if (sizes[slot] <= 0) current = null;
|
||||
else if (inventory[slot] != before || previous == null) {
|
||||
var stack = inventory[slot];
|
||||
current = stack.isEmpty() ? null : stack.getItem();
|
||||
}
|
||||
if (current != previous) {
|
||||
if (previous == null) universal.clear(slot);
|
||||
else if (indices != null) indices.get(previous).clear(slot);
|
||||
if (current == null) universal.set(slot);
|
||||
else if (indices != null) index(current).set(slot);
|
||||
kinds[slot] = current;
|
||||
reset(slot);
|
||||
} else if (sizes[slot] <= 0 && compared[slot]) reset(slot);
|
||||
}
|
||||
|
||||
private void exclude(int slot) {
|
||||
if (!eligible.get(slot)) return;
|
||||
eligible.clear(slot);
|
||||
for (var value : types.values()) {
|
||||
value.slots().clear(slot);
|
||||
for (var entry : value.rows().values()) entry.slots.clear(slot);
|
||||
}
|
||||
}
|
||||
|
||||
private void reset(int slot) {
|
||||
// Overflow can empty a virtual slot and permit assignment to another item.
|
||||
for (var value : types.values()) {
|
||||
value.matches()[slot] = 0;
|
||||
boolean allowed =
|
||||
eligible.get(slot)
|
||||
&& (value.key().empty()
|
||||
|| kinds[slot] == null
|
||||
|| value.key().item() == kinds[slot]);
|
||||
value.slots().set(slot, allowed);
|
||||
for (var entry : value.rows().values()) {
|
||||
entry.slots.set(slot, allowed && entry.valid[slot] != INVALID);
|
||||
entry.line = null;
|
||||
}
|
||||
}
|
||||
compared[slot] = false;
|
||||
}
|
||||
}
|
||||
42
server/src/main/java/dev/quorate/tune/TreeCodec.java
Normal file
42
server/src/main/java/dev/quorate/tune/TreeCodec.java
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.DataResult;
|
||||
import com.mojang.serialization.Dynamic;
|
||||
import com.mojang.serialization.DynamicOps;
|
||||
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.NbtOps;
|
||||
import net.minecraft.resources.RegistryOps;
|
||||
|
||||
public final class TreeCodec implements Codec<CompoundTag> {
|
||||
private final Codec<CompoundTag> original;
|
||||
|
||||
public TreeCodec(Codec<CompoundTag> original) {
|
||||
this.original = original;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> DataResult<Pair<CompoundTag, T>> decode(DynamicOps<T> ops, T input) {
|
||||
return original.decode(ops, input);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> DataResult<T> encode(CompoundTag input, DynamicOps<T> ops, T prefix) {
|
||||
if (ops.getClass() == RegistryOps.class
|
||||
&& ops instanceof RegistryOps<?> registry
|
||||
&& registry.withParent(NbtOps.INSTANCE) == registry) {
|
||||
var converted = NbtTree.convert(input);
|
||||
if (converted != null)
|
||||
return Codec.PASSTHROUGH.encode(new Dynamic<>(ops, (T) converted), ops, prefix);
|
||||
}
|
||||
return original.encode(input, ops, prefix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return original.toString();
|
||||
}
|
||||
}
|
||||
6
server/src/main/java/dev/quorate/tune/TuneMod.java
Normal file
6
server/src/main/java/dev/quorate/tune/TuneMod.java
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
package dev.quorate.tune;
|
||||
|
||||
import net.neoforged.fml.common.Mod;
|
||||
|
||||
@Mod("saltatory")
|
||||
public final class TuneMod {}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package dev.quorate.tune.mixin;
|
||||
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Overwrite;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Mixin(targets = "com.frikinjay.almanac.util.ItemNBTUtil", remap = false)
|
||||
public abstract class AlmanacMixin {
|
||||
@Redirect(
|
||||
method = "checkAndFix",
|
||||
at =
|
||||
@At(
|
||||
value = "INVOKE",
|
||||
target = "Ljava/util/Set;contains(Ljava/lang/Object;)Z",
|
||||
ordinal = 0),
|
||||
require = 1,
|
||||
allow = 1)
|
||||
private static boolean mctune$unchanged(Set<?> cache, Object item, ItemStack stack) {
|
||||
var data = stack.get(DataComponents.CUSTOM_DATA);
|
||||
// The exclusion branch avoids cancellation callbacks when custom data cannot change.
|
||||
return data == null || !data.isEmpty() || cache.contains(item);
|
||||
}
|
||||
|
||||
@Overwrite(remap = false)
|
||||
private static boolean tryRemoveEmptyCustomData(ItemStack stack) {
|
||||
var data = stack.get(DataComponents.CUSTOM_DATA);
|
||||
if (data != null && data.isEmpty()) {
|
||||
stack.remove(DataComponents.CUSTOM_DATA);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue