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/
|
**/.gradle/
|
||||||
/build/
|
**/build/
|
||||||
/runs/
|
**/runs/
|
||||||
/.idea/
|
/.idea/
|
||||||
*.iml
|
*.iml
|
||||||
/docs/journal
|
/docs/journal
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# License
|
# 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).
|
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
|
# 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. |
|
| [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. |
|
| [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.
|
Targets Minecraft 1.21.1 on GraalVM CE 25. Local builds require JDK 25 and Gradle 9.1.0.
|
||||||
|
|
||||||
```sh
|
```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)
|
[License](LICENSE.md) · [Third-party notices](NOTICE.md)
|
||||||
|
|
|
||||||
141
build.gradle
141
build.gradle
|
|
@ -1,74 +1,35 @@
|
||||||
plugins {
|
plugins {
|
||||||
id 'java-library'
|
id 'base'
|
||||||
id 'checkstyle'
|
id 'net.neoforged.gradle.userdev' version '7.1.39' apply false
|
||||||
id 'net.neoforged.gradle.userdev' version '7.1.39'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'dev.quorate'
|
subprojects { mod ->
|
||||||
version = '0.42.1'
|
apply plugin: 'java-library'
|
||||||
def modVersion = version.toString()
|
apply plugin: 'checkstyle'
|
||||||
|
apply plugin: 'net.neoforged.gradle.userdev'
|
||||||
|
|
||||||
java {
|
group = 'dev.quorate'
|
||||||
|
java {
|
||||||
toolchain.languageVersion = JavaLanguageVersion.of(25)
|
toolchain.languageVersion = JavaLanguageVersion.of(25)
|
||||||
withSourcesJar()
|
withSourcesJar()
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
ivy {
|
|
||||||
url = 'https://github.com/tlaplus/tlaplus/releases/download'
|
|
||||||
patternLayout { artifact '[revision]/[artifact].[ext]' }
|
|
||||||
metadataSources { artifact() }
|
|
||||||
content { includeModule 'org.lamport', 'tla2tools' }
|
|
||||||
}
|
}
|
||||||
mavenCentral()
|
repositories { mavenCentral() }
|
||||||
maven {
|
configurations { javafmt }
|
||||||
url = 'https://maven.blamejared.com/'
|
dependencies {
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
configurations { javafmt }
|
|
||||||
dependencies {
|
|
||||||
implementation 'net.neoforged:neoforge:21.1.250'
|
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'
|
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.4'
|
||||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.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 }
|
javafmt('com.google.googlejavaformat:google-java-format:1.37.0:all-deps') { transitive = false }
|
||||||
}
|
}
|
||||||
|
checkstyle {
|
||||||
checkstyle {
|
|
||||||
toolVersion = '14.3.0'
|
toolVersion = '14.3.0'
|
||||||
configFile = file('quality.xml')
|
configFile = rootProject.file('quality.xml')
|
||||||
}
|
}
|
||||||
|
|
||||||
def javaSources = fileTree('src') { include '**/*.java' }
|
def javaSources = fileTree('src') { include '**/*.java' }
|
||||||
['format': ['--replace'], 'formatCheck': ['--dry-run', '--set-exit-if-changed']].each { name, mode ->
|
['format': ['--replace'], 'formatCheck': ['--dry-run', '--set-exit-if-changed']].each { name, mode ->
|
||||||
tasks.register(name, JavaExec) {
|
tasks.register(name, JavaExec) {
|
||||||
|
inputs.files(javaSources).skipWhenEmpty()
|
||||||
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) }
|
javaLauncher = javaToolchains.launcherFor { languageVersion = JavaLanguageVersion.of(25) }
|
||||||
classpath = configurations.javafmt
|
classpath = configurations.javafmt
|
||||||
mainClass = 'com.google.googlejavaformat.java.Main'
|
mainClass = 'com.google.googlejavaformat.java.Main'
|
||||||
|
|
@ -77,64 +38,36 @@ def javaSources = fileTree('src') { include '**/*.java' }
|
||||||
}
|
}
|
||||||
doFirst { args = ['--aosp'] + mode + javaSources.files.sort().collect { it.path } }
|
doFirst { args = ['--aosp'] + mode + javaSources.files.sort().collect { it.path } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tasks.named('formatCheck') { mustRunAfter 'format' }
|
tasks.named('formatCheck') { mustRunAfter 'format' }
|
||||||
tasks.withType(Checkstyle).configureEach { 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('check') { dependsOn 'quality' }
|
||||||
tasks.named('checkstyleProbe') {
|
tasks.named('test') {
|
||||||
dependsOn 'compileProbeJava'
|
|
||||||
classpath = sourceSets.probe.compileClasspath
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named('test') {
|
|
||||||
dependsOn 'quality'
|
dependsOn 'quality'
|
||||||
inputs.dir('src/test/tla')
|
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
maxHeapSize = '512m'
|
maxHeapSize = '512m'
|
||||||
jvmArgs '-XX:+UseZGC'
|
jvmArgs '-XX:+UseZGC'
|
||||||
}
|
}
|
||||||
|
tasks.named('processResources') {
|
||||||
tasks.named('processResources') {
|
inputs.property 'version', provider { mod.version.toString() }
|
||||||
inputs.property 'version', modVersion
|
filesMatching('META-INF/neoforge.mods.toml') { expand version: mod.version.toString() }
|
||||||
filesMatching('META-INF/neoforge.mods.toml') { expand version: modVersion }
|
}
|
||||||
}
|
tasks.withType(JavaCompile).matching { it.name in ['compileJava', 'compileTestJava', 'compileProbeJava'] }.configureEach {
|
||||||
|
|
||||||
['compileJava', 'compileTestJava', 'compileProbeJava'].each { name ->
|
|
||||||
tasks.named(name, JavaCompile) {
|
|
||||||
options.encoding = 'UTF-8'
|
options.encoding = 'UTF-8'
|
||||||
options.compilerArgs += ['-Xlint:all', '-Werror', '-proc:none']
|
options.compilerArgs += ['-Xlint:all', '-Werror', '-proc:none']
|
||||||
mustRunAfter 'format'
|
mustRunAfter 'format'
|
||||||
}
|
}
|
||||||
}
|
tasks.withType(Jar).configureEach {
|
||||||
|
|
||||||
tasks.withType(Jar).configureEach {
|
|
||||||
preserveFileTimestamps = false
|
preserveFileTimestamps = false
|
||||||
reproducibleFileOrder = true
|
reproducibleFileOrder = true
|
||||||
from(['LICENSE.md', 'COPYING', 'COPYING.LESSER', 'NOTICE.md']) { into 'META-INF' }
|
from(['LICENSE.md', 'COPYING', 'COPYING.LESSER', 'NOTICE.md'].collect { rootProject.file(it) }) { 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'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('probeJar', Jar) {
|
['format', 'formatCheck', 'quality'].each { name ->
|
||||||
dependsOn 'quality', 'probeClasses'
|
tasks.register(name) { dependsOn subprojects.collect { it.tasks.named(name) } }
|
||||||
archiveBaseName = 'mc-checks'
|
}
|
||||||
destinationDirectory = layout.buildDirectory.dir('probe')
|
['assemble', 'check'].each { name ->
|
||||||
from sourceSets.probe.output
|
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