Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d6b2c05
Optomize memory usage and cache usage
Trainboy15 May 18, 2026
6952825
Update version from 1.0.1-beta to 1.0.0-patch
Trainboy15 May 18, 2026
743151f
Remove gradle temp
Trainboy15 May 18, 2026
0a82893
Refactor Gradle CI workflow for improved structure
Trainboy15 May 18, 2026
f499758
Update build artifact
invalid-email-address May 18, 2026
ecedd55
Remove deployment step from gradle.yml
Trainboy15 May 18, 2026
35d2da4
Update build artifact
invalid-email-address May 18, 2026
52ea90c
Fix config
Trainboy15 May 18, 2026
bcfb2b9
Add NPE catches
Trainboy15 May 18, 2026
a632e1f
Merge pull request #1 from Trainboy15/testing
Trainboy15 May 18, 2026
e6d7ee9
Update build artifact
invalid-email-address May 18, 2026
cd5988f
Tab indentation
Trainboy15 May 18, 2026
5b1cae7
Update build artifact
invalid-email-address May 18, 2026
abb5169
Clean up whitespace
UplandJacob May 18, 2026
ab3107e
Update build artifact
invalid-email-address May 18, 2026
b64bd0d
Fix IllegalArgumentException
Trainboy15 May 19, 2026
d5d0b4c
Update build artifact
invalid-email-address May 19, 2026
d7da218
Add update check
Trainboy15 May 19, 2026
c327678
Fixed update checker
Trainboy15 May 19, 2026
854f750
Update build artifact
invalid-email-address May 19, 2026
951fab7
Add testing... IDK if it will work
Trainboy15 May 19, 2026
ddd22ad
Change to java 17
Trainboy15 May 19, 2026
c97efac
Fix dependencies?
Trainboy15 May 19, 2026
8005e8f
FR this time trust
Trainboy15 May 19, 2026
509cf1b
Update build artifact
invalid-email-address May 19, 2026
2967f45
Refactor GitHub Actions workflow for tests
Trainboy15 May 19, 2026
21ac55b
Update build artifact
invalid-email-address May 19, 2026
acf5f53
maybe fix memory issuses
Trainboy15 May 20, 2026
b2a43a6
Update build artifact
invalid-email-address May 20, 2026
707b3cf
Impement @UplandJacob's changes
Trainboy15 May 21, 2026
844dbd9
Update build artifact
invalid-email-address May 21, 2026
9cc2862
undo some things for now
UplandJacob May 23, 2026
c23163c
Update build artifact
invalid-email-address May 23, 2026
e5c2f5f
oop more
UplandJacob May 23, 2026
fe9c89c
Update build artifact
invalid-email-address May 23, 2026
6427b1e
Remove unnecessary line from gradle.yml
UplandJacob May 23, 2026
fb46122
Update build artifact
invalid-email-address May 23, 2026
6a48b75
Implement some of @UplandJacob's changes
Trainboy15 May 26, 2026
0c404b3
Update build artifact
invalid-email-address May 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/test.yml
Comment thread
UplandJacob marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Build & Test

on:
push:
branches: [ "**" ]
pull_request:
branches: [ "**" ]

jobs:
test:
name: Unit Tests
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Java 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Run tests
run: ./gradlew test --no-daemon

- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports
path: build/reports/tests/test/
26 changes: 19 additions & 7 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,30 @@ repositories {
maven { url = 'https://repo.codemc.io/repository/maven-snapshots/' }
maven { url = 'https://jitpack.io' }
maven { url = 'https://repo.dmulloy2.net/repository/public/' }
maven { url = 'https://repo.seeseemelk.be/repository/maven-public/' } // MockBukkit
}

dependencies {
compileOnly 'org.spigotmc:spigot-api:1.18.2-R0.1-SNAPSHOT'

implementation 'com.github.retrooper:packetevents-spigot:2.11.1'

compileOnly 'com.viaversion:viabackwards:5.3.2'
compileOnly 'com.viaversion:viaversion:5.4.1'

compileOnly 'it.unimi.dsi:fastutil:8.5.16'

implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'

compileOnly 'io.netty:netty-all:4.1.97.Final'
compileOnly 'io.netty:netty-all:4.1.97.Final'

implementation 'org.java-websocket:Java-WebSocket:1.5.4'

compileOnly 'org.projectlombok:lombok:1.18.30'
annotationProcessor 'org.projectlombok:lombok:1.18.30'

testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
testImplementation 'com.github.seeseemelk:MockBukkit-v1.18:3.86.0'
}

processResources {
Expand All @@ -58,12 +62,12 @@ processResources {
shadowJar {
archiveClassifier.set('')
archiveFileName.set("${project.name}-${project.version}.jar")

relocate 'com.github.retrooper.packetevents', 'tf.tuff.packetevents'
relocate 'io.github.retrooper.packetevents', 'tf.tuff.packetevents'
relocate 'com.fasterxml.jackson', 'tf.tuff.jackson'
relocate 'org.java_websocket', 'tf.tuff.websocket'

exclude 'META-INF/*.SF'
exclude 'META-INF/*.DSA'
exclude 'META-INF/*.RSA'
Expand All @@ -80,3 +84,11 @@ tasks.named('jar') {
tasks.named('build') {
dependsOn shadowJar
}

test {
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
exceptionFormat "full"
}
}
Binary file modified builds/TuffXPlus-1.0.1-beta.jar
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because you've made this PR from your main branch, the Action keeps updating the builds. You should really create a separate branch to make this PR from.

Binary file not shown.
4 changes: 4 additions & 0 deletions src/main/java/tf/tuff/TuffX.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public class TuffX extends JavaPlugin implements Listener, PluginMessageListener
public ViaBlocksPlugin viaBlocksPlugin;
public TuffActions tuffActions;
public ViaEntitiesPlugin viaEntitiesPlugin;
public String latestAvailableVersion = null;

private ChunkInjector chunkInjector;

@Override
Expand Down Expand Up @@ -101,6 +103,8 @@ public void onDisable() {
}

PacketEvents.getAPI().terminate();

getServer().getMessenger().unregisterIncomingPluginChannel(this);
}

public void reloadTuffX(){
Expand Down
50 changes: 33 additions & 17 deletions src/main/java/tf/tuff/viablocks/CustomBlockListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@

import tf.tuff.viablocks.version.VersionAdapter;

import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongList;

public class CustomBlockListener {

public final ViaBlocksPlugin plugin;
Expand Down Expand Up @@ -83,6 +88,7 @@ public byte[] getCachedChunkData(String worldName, int x, int z) {
}

public void setChunkInjector(tf.tuff.netty.ChunkInjector injector) {
if (injector == null) return;
this.chunkInjector = injector;
}

Expand Down Expand Up @@ -258,21 +264,30 @@ public void cacheChunkWithCallback(World world, int x, int z, Consumer<byte[]> c
}

private Map<Integer, List<Long>> findModernBlocksInChunk(ChunkSnapshot chunkSnapshot, int minHeight, int maxHeight) {
Map<Integer, List<Long>> foundBlocks = new HashMap<>();
Int2ObjectMap<LongList> foundBlocks = new Int2ObjectOpenHashMap<>();

int chunkX = chunkSnapshot.getX() << 4;
int chunkZ = chunkSnapshot.getZ() << 4;

for (int y = minHeight; y < maxHeight; y++) {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {

for (int x = 0; x < 16; x++) {
int worldX = chunkX + x;
for (int z = 0; z < 16; z++) {
int worldZ = chunkZ + z;
for (int y = minHeight; y < maxHeight; y++) {

// Check material FIRST — getBlockType() returns an enum, no allocation
Material blockType = chunkSnapshot.getBlockType(x, y, z);
if (blockType == Material.AIR || !this.modernMaterials.contains(blockType)) {

if (blockType == Material.AIR
|| blockType == Material.CAVE_AIR
|| blockType == Material.VOID_AIR
|| !this.modernMaterials.contains(blockType)) {
continue;
}
@SuppressWarnings("null")
@Nonnull BlockData data = chunkSnapshot.getBlockData(x, y, z);

// Only allocate BlockData for confirmed modern blocks
BlockData data = chunkSnapshot.getBlockData(x, y, z);

Integer cachedId = blockDataIdCache.getIfPresent(data);
int materialId;
if (cachedId != null) {
Expand All @@ -281,22 +296,23 @@ private Map<Integer, List<Long>> findModernBlocksInChunk(ChunkSnapshot chunkSnap
materialId = this.paletteManager.getOrCreateId(data.getAsString());
blockDataIdCache.put(data, materialId);
}

if (materialId != -1) {
long packedLocation = packLocation(chunkX + x, y, chunkZ + z);
List<Long> locs = foundBlocks.get(materialId);
long packedLocation = packLocation(worldX, y, worldZ);
LongList locs = foundBlocks.get(materialId);
if (locs == null) {
locs = new ArrayList<>();
locs = new LongArrayList();
foundBlocks.put(materialId, locs);
}
locs.add(packedLocation);
}
}
}
}
return foundBlocks;
}


return (Map<Integer, List<Long>>) (Map<?, ?>) foundBlocks;
}

public byte[] getExtraDataForMultiBlock(World world, List<Long> locations) {
Map<Integer, List<Long>> foundBlocks = new HashMap<>();

Expand Down
31 changes: 20 additions & 11 deletions src/main/java/tf/tuff/y0/Y0Plugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ public boolean isPlayerReady(Player p) {
}

public void setChunkInjector(tf.tuff.netty.ChunkInjector injector) {
if (injector == null) return;
this.chunkInjector = injector;
}

Expand Down Expand Up @@ -689,49 +690,57 @@ public void handleChunkLoad(org.bukkit.event.world.ChunkLoadEvent e) {
}

private byte[] csp(ChunkSnapshot s, int x, int z, int sy, Object2ObjectOpenHashMap<BlockData, int[]> c) throws IOException {
// Ensure thread-local buffer is exactly 12,288 bytes to prevent overflow
byte[] bd = tlbd.get();
int idx = 0;
boolean h = false;
boolean hasContent = false;
Comment thread
Trainboy15 marked this conversation as resolved.
int by = sy << 4;

for (int y = 0; y < 16; y++) {
int wy = by + y;
// Optimized Loop Order: Matches standard Minecraft internal memory layouts
for (int xx = 0; xx < 16; xx++) {
for (int zz = 0; zz < 16; zz++) {
for (int xx = 0; xx < 16; xx++) {
for (int y = 0; y < 16; y++) {
int wy = by + y;

BlockData bdata = s.getBlockData(xx, wy, zz);
int[] ld = c.getOrDefault(bdata, EMPTY_LEGACY);
if (ld == EMPTY_LEGACY && v != null) {
ld = v.toLegacy(bdata);
int[] ld = c.get(bdata); // Fast map lookup

if (ld == null) { // Avoid getOrDefault overhead
ld = (v != null) ? v.toLegacy(bdata) : EMPTY_LEGACY;
c.put(bdata, ld);
}

// Bitwise packing
short lb = (short) ((ld[1] << 12) | (ld[0] & 0xFFF));
byte pl = (byte) ((s.getBlockSkyLight(xx, wy, zz) << 4) | s.getBlockEmittedLight(xx, wy, zz));

// Write sequence
bd[idx++] = (byte) (lb >> 8);
bd[idx++] = (byte) lb;
bd[idx++] = pl;

if (lb != 0 || pl != 0) {
h = true;
hasContent = true;
}
}
}
}

if (!h) return null;
if (!hasContent) return null;

ByteArrayOutputStream b = tlos.get();
b.reset();

// DataOutputStream wrapper safely writes schema
try (DataOutputStream o = new DataOutputStream(b)) {
o.writeUTF("chunk_data");
o.writeInt(x);
o.writeInt(z);
o.writeInt(sy);
o.write(bd, 0, idx);
return b.toByteArray();
}

return b.toByteArray();
}

public void handleBlockBreak(BlockBreakEvent e) {
Expand Down Expand Up @@ -903,4 +912,4 @@ private byte[] clp(ChunkSnapshot s, CSC sc) throws IOException {
}
}

}
}
66 changes: 66 additions & 0 deletions src/test/TuffXTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package tf.tuff;

import be.seeseemelk.mockbukkit.MockBukkit;
import be.seeseemelk.mockbukkit.ServerMock;
import be.seeseemelk.mockbukkit.entity.PlayerMock;
import org.junit.jupiter.api.*;

import static org.junit.jupiter.api.Assertions.*;

class TuffXTest {

private static ServerMock server;
private static TuffX plugin;

@BeforeEach
void setUp() {
server = MockBukkit.mock();
plugin = MockBukkit.load(TuffX.class);
}

@AfterEach
void tearDown() {
MockBukkit.unmock();
}

@Test
void pluginEnablesSuccessfully() {
assertTrue(plugin.isEnabled(), "Plugin should be enabled after load");
}

@Test
void latestVersionIsNullOnStartup() {
assertNull(plugin.latestAvailableVersion,
"No update should be detected synchronously at startup");
}

@Test
void reloadDoesNotThrow() {
assertDoesNotThrow(() -> plugin.reloadTuffX(),
"reloadTuffX() should not throw");
}

@Test
void opReceivesUpdateMessageWhenUpdateAvailable() {
plugin.latestAvailableVersion = "2.0.0";

PlayerMock player = server.addPlayer();
player.setOp(true);
player.assertNoMoreSaid();

player.disconnect();
server.addPlayer(player.getName());

player.assertSaid("§e[TuffX] §fA new version is available: §a2.0.0 §f(running §c1.0.0-patch§f)");
}

@Test
void nonOpDoesNotReceiveUpdateMessage() {
plugin.latestAvailableVersion = "2.0.0";

PlayerMock player = server.addPlayer();
player.setOp(false);

player.assertNoMoreSaid();
}
}