Compare commits

...

19 Commits

Author SHA1 Message Date
Spottedleaf
206255e9f5 Set version to 0.2.0-beta.11 2025-06-20 18:45:06 -07:00
Spottedleaf
40a9f1cfcd Do not allow ticket level decreases to be processed asynchronously
Note: This cannot happen on the Fabric/NeoForge versions since
async ticket level processing is not allowed, but can happen on
Paper. This change is made here so that Paper can
remain in sync.

Ticket level decreases may be handled asynchronously when the
off-thread invokes processTicketUpdates() when the main thread
is running ChunkHolderManager#tick(). This is because the ticket
update is queued during tick(), but not executed (invoking
processTicketUpdates) until after releasing the ticket lock.
This creates a small window for an off-thread to invoke
processTicketUpdates() and steal the update.

When the update is stolen, the full chunk status update (if any)
will be eventually queued to execute via the chunk task queue.
If the chunk queue is processed during the server tick at any
point other than the ChunkHolderManager tick, then any ticket
level decrease will violate an important invariant in the
Moonrise chunk system: ticket level decreases only occur during
ChunkHolderManager tick. This invariant exists to make interfacing
with the chunk system easier, especially working with off-thread
contexts.

This change is specifically made to work towards fixing
https://github.com/PaperMC/Folia/issues/363
2025-06-20 18:41:53 -07:00
Spottedleaf
b4789a91f9 Add chunk unload delay config option
The default config option is no unload delay, so that
it maintains parity with Vanilla.

Fixes https://github.com/Tuinity/Moonrise/issues/115
2025-06-20 18:41:00 -07:00
Spottedleaf
97b84f0ccc Update neoforge/fabric, fix incompatibility with "Fast IP Ping"
Inject in a different place to prevent the mixin from failing to
apply. It looks like we do essentially the same logic anyways.
2025-06-20 14:46:00 -07:00
Spottedleaf
1470211fe3 Set version to 0.2.0-SNAPSHOT 2025-06-09 02:23:10 -07:00
Spottedleaf
4d3b4a52a6 Set version to 0.2.0-beta.10 2025-06-09 02:20:27 -07:00
Spottedleaf
a746449be8 Fix infinite loop in RegionFile IO
If an exception is thrown during decompress then the read process
would be started again, which of course would eventually throw in
the decompress process.
2025-06-09 02:19:37 -07:00
Spottedleaf
13948cdf26 Set version to 0.2.0-SNAPSHOT 2025-02-24 21:09:35 -08:00
Spottedleaf
ac0c7deb43 Set version to 0.2.0-beta.9 2025-02-24 21:06:48 -08:00
Jason Penilla
16c8398d8a Clear lastSection on game event listener removal (fixes #87) (#99) 2025-02-24 17:39:07 -07:00
Spottedleaf
661ef813bb Add further information to thread check errors
The entity data is more complete, which will help debug problems
on Folia.
2025-01-28 13:34:32 -08:00
Spottedleaf
cf1d26a73c Set version to 0.2.0-SNAPSHOT 2025-01-27 13:59:26 -08:00
Spottedleaf
0cbff02a1c Set version to 0.2.0-beta.8 2025-01-27 13:57:16 -08:00
Spottedleaf
ce4ee767fe Correctly retrun true for empty input shapes in EntityGetter#isUnobstructed
Vanilla will return true for empty shapes, so we should as well.
2025-01-27 07:51:49 -08:00
Jason Penilla
d31b15122f build: Sort AT output instead of copying and modifying AT classes 2025-01-17 19:38:38 -07:00
Jason Penilla
ca931e842b build: Enable config cache, replace deprecated space assignment use (#92) 2025-01-17 19:24:18 -07:00
Spottedleaf
c2cf985899 Update to ConcurrentUtil 0.0.3 2025-01-11 06:26:19 -08:00
Spottedleaf
d270cf06d9 Log thread check parameters when the thread check fails
This provides additional debug information that may be useful.
2025-01-11 04:52:21 -08:00
Spottedleaf
09735958c0 Set version to 0.2.0-SNAPSHOT 2024-12-23 00:06:18 -08:00
17 changed files with 249 additions and 438 deletions

View File

@@ -68,19 +68,19 @@ allprojects {
repositories {
maven {
url "https://repo.papermc.io/repository/maven-public/"
url = "https://repo.papermc.io/repository/maven-public/"
mavenContent {
includeGroup("ca.spottedleaf")
}
}
maven {
url "https://api.modrinth.com/maven"
url = "https://api.modrinth.com/maven"
mavenContent {
includeGroup("maven.modrinth")
}
}
maven { url "https://maven.shedaniel.me/" }
maven { url "https://maven.terraformersmc.com/releases/" }
maven { url = "https://maven.shedaniel.me/" }
maven { url = "https://maven.terraformersmc.com/releases/" }
}
// make build reproducible

View File

@@ -2,11 +2,17 @@ import dev.architectury.at.AccessChange;
import dev.architectury.at.AccessTransform;
import dev.architectury.at.AccessTransformSet;
import dev.architectury.at.ModifierChange;
import dev.architectury.at.io.AccessTransformFormat;
import dev.architectury.at.io.AccessTransformFormats;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
import net.fabricmc.accesswidener.AccessWidenerReader;
import net.fabricmc.accesswidener.AccessWidenerVisitor;
@@ -66,12 +72,29 @@ public abstract class Aw2AtTask extends DefaultTask {
final AccessTransformSet accessTransformSet = toAccessTransformSet(reader);
Files.deleteIfExists(this.getOutputFile().get().getAsFile().toPath());
Files.createDirectories(this.getOutputFile().get().getAsFile().toPath().getParent());
AccessTransformFormats.FML.write(this.getOutputFile().get().getAsFile().toPath(), accessTransformSet);
writeLF(AccessTransformFormats.FML, this.getOutputFile().get().getAsFile().toPath(), accessTransformSet);
} catch (final IOException e) {
throw new RuntimeException(e);
}
}
private static void writeLF(final AccessTransformFormat format, final Path path, final AccessTransformSet at) throws IOException {
final StringWriter stringWriter = new StringWriter();
final BufferedWriter writer = new BufferedWriter(stringWriter);
format.write(writer, at);
writer.close();
final List<String> lines = Arrays.stream(stringWriter.toString()
// unify line endings
.replace("\r\n", "\n")
.split("\n"))
// skip blank lines
.filter(it -> !it.isBlank())
// sort
.sorted()
.toList();
Files.writeString(path, String.join("\n", lines));
}
// Below methods are heavily based on architectury-loom Aw2At class (MIT licensed)
/*
MIT License
@@ -98,8 +121,7 @@ public abstract class Aw2AtTask extends DefaultTask {
*/
public static AccessTransformSet toAccessTransformSet(final BufferedReader reader) throws IOException {
// TODO: Remove copied classes once https://github.com/architectury/at/pull/1 is released
AccessTransformSet atSet = new at.AccessTransformSetImpl();
AccessTransformSet atSet = AccessTransformSet.create();
new AccessWidenerReader(new AccessWidenerVisitor() {
@Override

View File

@@ -1,97 +0,0 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2023 Architectury
* Copyright (c) 2018 Minecrell (https://github.com/Minecrell)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package at;
import dev.architectury.at.AccessTransformSet;
import net.fabricmc.mappingio.tree.MappingTreeView;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
public class AccessTransformSetImpl implements AccessTransformSet {
private final Map<String, Class> classes = new LinkedHashMap<>();
@Override
public Map<String, Class> getClasses() {
return Collections.unmodifiableMap(this.classes);
}
@Override
public Optional<Class> getClass(String name) {
Objects.requireNonNull(name, "name");
return Optional.ofNullable(this.classes.get(name.replace('.', '/')));
}
@Override
public Class getOrCreateClass(String name) {
Objects.requireNonNull(name, "name");
return this.classes.computeIfAbsent(name.replace('.', '/'), n -> new ClassAccessTransformSetImpl(this, n));
}
@Override
public Optional<Class> removeClass(String name) {
Objects.requireNonNull(name, "name");
return Optional.ofNullable(this.classes.remove(name.replace('.', '/')));
}
@Override
public AccessTransformSet remap(MappingTreeView mappings, String from, String to) {
throw new UnsupportedOperationException();
}
@Override
public void merge(AccessTransformSet other) {
other.getClasses().forEach((name, classSet) -> getOrCreateClass(name).merge(classSet));
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof AccessTransformSetImpl)) {
return false;
}
AccessTransformSetImpl that = (AccessTransformSetImpl) o;
return this.classes.equals(that.classes);
}
@Override
public int hashCode() {
return this.classes.hashCode();
}
@Override
public String toString() {
return "AccessTransformSet{" + classes + '}';
}
}

View File

@@ -1,256 +0,0 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2023 Architectury
* Copyright (c) 2018 Minecrell (https://github.com/Minecrell)
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package at;
import dev.architectury.at.AccessTransform;
import dev.architectury.at.AccessTransformSet;
import org.cadixdev.bombe.analysis.InheritanceProvider;
import org.cadixdev.bombe.type.signature.MethodSignature;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.StringJoiner;
class ClassAccessTransformSetImpl implements AccessTransformSet.Class {
private final AccessTransformSet parent;
private final String name;
private AccessTransform classTransform = AccessTransform.EMPTY;
private AccessTransform allFields = AccessTransform.EMPTY;
private AccessTransform allMethods = AccessTransform.EMPTY;
private final Map<String, AccessTransform> fields = new LinkedHashMap<>();
private final Map<MethodSignature, AccessTransform> methods = new LinkedHashMap<>();
private boolean complete;
ClassAccessTransformSetImpl(AccessTransformSet parent, String name) {
this.parent = parent;
this.name = name;
}
@Override
public AccessTransformSet getParent() {
return parent;
}
@Override
public String getName() {
return name;
}
@Override
public AccessTransform get() {
return this.classTransform;
}
@Override
public AccessTransform merge(AccessTransform transform) {
return this.classTransform = this.classTransform.merge(transform);
}
@Override
public AccessTransform replace(AccessTransform transform) {
return this.classTransform = Objects.requireNonNull(transform, "transform");
}
@Override
public AccessTransform allFields() {
return this.allFields;
}
@Override
public AccessTransform mergeAllFields(AccessTransform transform) {
return this.allFields = this.allFields.merge(transform);
}
@Override
public AccessTransform replaceAllFields(AccessTransform transform) {
return this.allFields = Objects.requireNonNull(transform, "transform");
}
@Override
public AccessTransform allMethods() {
return this.allMethods;
}
@Override
public AccessTransform mergeAllMethods(AccessTransform transform) {
return this.allMethods = this.allMethods.merge(transform);
}
@Override
public AccessTransform replaceAllMethods(AccessTransform transform) {
return this.allMethods = Objects.requireNonNull(transform, "transform");
}
@Override
public Map<String, AccessTransform> getFields() {
return Collections.unmodifiableMap(this.fields);
}
@Override
public AccessTransform getField(String name) {
return this.fields.getOrDefault(Objects.requireNonNull(name, "name"), this.allFields);
}
@Override
public AccessTransform mergeField(String name, AccessTransform transform) {
Objects.requireNonNull(name, "name");
Objects.requireNonNull(transform, "transform");
if (transform.isEmpty()) {
return this.fields.getOrDefault(name, AccessTransform.EMPTY);
}
return this.fields.merge(name, transform, AccessTransform::merge);
}
@Override
public AccessTransform replaceField(String name, AccessTransform transform) {
Objects.requireNonNull(name, "name");
Objects.requireNonNull(transform, "transform");
if (transform.isEmpty()) {
return this.fields.remove(name);
}
return this.fields.put(name, transform);
}
@Override
public Map<MethodSignature, AccessTransform> getMethods() {
return Collections.unmodifiableMap(this.methods);
}
@Override
public AccessTransform getMethod(MethodSignature signature) {
return this.methods.getOrDefault(Objects.requireNonNull(signature, "signature"), this.allMethods);
}
@Override
public AccessTransform mergeMethod(MethodSignature signature, AccessTransform transform) {
Objects.requireNonNull(signature, "signature");
Objects.requireNonNull(transform, "transform");
if (transform.isEmpty()) {
return this.methods.getOrDefault(signature, AccessTransform.EMPTY);
}
return this.methods.merge(signature, transform, AccessTransform::merge);
}
@Override
public AccessTransform replaceMethod(MethodSignature signature, AccessTransform transform) {
Objects.requireNonNull(signature, "signature");
Objects.requireNonNull(transform, "transform");
if (transform.isEmpty()) {
return this.methods.remove(signature);
}
return this.methods.put(signature, transform);
}
@Override
public void merge(AccessTransformSet.Class other) {
Objects.requireNonNull(other, "other");
merge(other.get());
mergeAllFields(other.allFields());
mergeAllMethods(other.allMethods());
other.getFields().forEach(this::mergeField);
other.getMethods().forEach(this::mergeMethod);
}
@Override
public boolean isComplete() {
return this.complete;
}
@Override
public void complete(InheritanceProvider provider, InheritanceProvider.ClassInfo info) {
if (this.complete) {
return;
}
for (InheritanceProvider.ClassInfo parent : info.provideParents(provider)) {
AccessTransformSet.Class parentAts = getParent().getOrCreateClass(parent.getName());
parentAts.complete(provider, parent);
parentAts.getMethods().forEach((signature, transform) -> {
if (info.overrides(signature, parent)) {
mergeMethod(signature, transform);
}
});
}
this.complete = true;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ClassAccessTransformSetImpl)) {
return false;
}
ClassAccessTransformSetImpl that = (ClassAccessTransformSetImpl) o;
return this.classTransform.equals(that.classTransform) &&
this.allFields.equals(that.allFields) &&
this.allMethods.equals(that.allMethods) &&
this.fields.equals(that.fields) &&
this.methods.equals(that.methods);
}
@Override
public int hashCode() {
return Objects.hash(this.classTransform, this.allFields, this.allMethods, this.fields, this.methods);
}
@Override
public String toString() {
StringJoiner joiner = new StringJoiner(", ", "AccessTransformSet.Class{", "}");
if (!this.classTransform.isEmpty()) {
joiner.add(this.classTransform.toString());
}
if (!this.allFields.isEmpty()) {
joiner.add("allFields=" + this.allFields);
}
if (!this.allMethods.isEmpty()) {
joiner.add("allMethods=" + this.allMethods);
}
if (!this.fields.isEmpty()) {
joiner.add("fields=" + this.fields);
}
if (!this.methods.isEmpty()) {
joiner.add("method=" + this.methods);
}
return joiner.toString();
}
}

View File

@@ -2,16 +2,17 @@
org.gradle.jvmargs=-Xmx2G
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
# Fabric Properties
# check these on https://modmuss50.me/fabric.html
minecraft_version=1.21.4
loader_version=0.16.9
supported_minecraft_versions=1.21.4
neoforge_version=21.4.33-beta
neoforge_version=21.4.140
neoform_version=1.21.4-20241203.161809
fabric_api_version=0.110.5+1.21.4
fabric_api_version=0.119.3+1.21.4
snakeyaml_version=2.3
concurrentutil_version=0.0.2
concurrentutil_version=0.0.3
yamlconfig_version=1.0.2
cloth_version=17.0.144
modmenu_version=13.0.0-beta.1
@@ -20,6 +21,6 @@ junit_version=5.11.3
fabric_lithium_version=t1FlWYl9
neo_lithium_version=iDqQi66g
# Mod Properties
mod_version=0.2.0-beta.7
mod_version=0.2.0-beta.11
maven_group=ca.spottedleaf.moonrise
archives_base_name=moonrise

View File

@@ -2,6 +2,7 @@ package ca.spottedleaf.moonrise.common.config.moonrise;
import ca.spottedleaf.moonrise.common.config.ui.ClothConfig;
import ca.spottedleaf.moonrise.common.util.MoonriseCommon;
import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import ca.spottedleaf.yamlconfig.InitialiseHook;
import ca.spottedleaf.yamlconfig.annotation.Adaptable;
@@ -38,7 +39,7 @@ public final class MoonriseConfig {
@Adaptable
public static final class Basic {
public static final class Basic implements InitialiseHook {
@Serializable(
comment = """
The maximum rate of chunks to send to any given player, per second. If this value is <= 0,
@@ -72,6 +73,20 @@ public final class MoonriseConfig {
section = CHUNK_SYSTEM_SECTION
)
public double playerMaxGenRate = -1.0;
@Serializable(
comment = """
The delay before chunks are unloaded around players once they leave their view distance.
The Vanilla value is 0 ticks. Setting this value higher (i.e 5s) will allow pets to teleport
to their owners when they teleport.
"""
)
public Duration playerChunkUnloadDelay = Duration.parse("0t");
@Override
public void initialise() {
RegionizedPlayerChunkLoader.setUnloadDelay(this.playerChunkUnloadDelay.getTimeTicks());
}
}
@Serializable(

View File

@@ -0,0 +1,44 @@
package ca.spottedleaf.moonrise.common.util;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.Vec3;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
public final class EntityUtil {
private static final ThreadLocal<DecimalFormat> THREE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> {
return new DecimalFormat("#,##0.000");
});
private static String formatVec(final Vec3 vec) {
final DecimalFormat format = THREE_DECIMAL_PLACES.get();
return "(" + format.format(vec.x) + "," + format.format(vec.y) + "," + format.format(vec.z) + ")";
}
private static String dumpEntityWithoutReferences(final Entity entity) {
if (entity == null) {
return "{null}";
}
return "{type=" + entity.getClass().getSimpleName() + ",id=" + entity.getId() + ",uuid=" + entity.getUUID() + ",pos=" + formatVec(entity.position())
+ ",mot=" + formatVec(entity.getDeltaMovement()) + ",aabb=" + entity.getBoundingBox() + ",removed=" + entity.getRemovalReason() + ",has_vehicle=" + (entity.getVehicle() != null)
+ ",passenger_count=" + entity.getPassengers().size();
}
public static String dumpEntity(final Entity entity) {
final List<Entity> passengers = entity.getPassengers();
final List<String> passengerStrings = new ArrayList<>(passengers.size());
for (final Entity passenger : passengers) {
passengerStrings.add("(" + dumpEntityWithoutReferences(passenger) + ")");
}
return "{root=[" + dumpEntityWithoutReferences(entity) + "], vehicle=[" + dumpEntityWithoutReferences(entity.getVehicle())
+ "], passengers=[" + String.join(",", passengerStrings) + "]";
}
private EntityUtil() {}
}

View File

@@ -15,56 +15,81 @@ public class TickThread extends Thread {
private static final Logger LOGGER = LoggerFactory.getLogger(TickThread.class);
private static String getThreadContext() {
return "thread=" + Thread.currentThread().getName();
}
/**
* @deprecated
*/
@Deprecated
public static void ensureTickThread(final String reason) {
if (!isTickThread()) {
LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
LOGGER.error("Thread failed main thread check: " + reason + ", context=" + getThreadContext(), new Throwable());
throw new IllegalStateException(reason);
}
}
public static void ensureTickThread(final Level world, final BlockPos pos, final String reason) {
if (!isTickThreadFor(world, pos)) {
LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
throw new IllegalStateException(reason);
final String ex = "Thread failed main thread check: " +
reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + pos;
LOGGER.error(ex, new Throwable());
throw new IllegalStateException(ex);
}
}
public static void ensureTickThread(final Level world, final BlockPos pos, final int blockRadius, final String reason) {
if (!isTickThreadFor(world, pos, blockRadius)) {
final String ex = "Thread failed main thread check: " +
reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + pos + ", block_radius=" + blockRadius;
LOGGER.error(ex, new Throwable());
throw new IllegalStateException(ex);
}
}
public static void ensureTickThread(final Level world, final ChunkPos pos, final String reason) {
if (!isTickThreadFor(world, pos)) {
LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
throw new IllegalStateException(reason);
final String ex = "Thread failed main thread check: " +
reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", chunk_pos=" + pos;
LOGGER.error(ex, new Throwable());
throw new IllegalStateException(ex);
}
}
public static void ensureTickThread(final Level world, final int chunkX, final int chunkZ, final String reason) {
if (!isTickThreadFor(world, chunkX, chunkZ)) {
LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
throw new IllegalStateException(reason);
final String ex = "Thread failed main thread check: " +
reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", chunk_pos=" + new ChunkPos(chunkX, chunkZ);
LOGGER.error(ex, new Throwable());
throw new IllegalStateException(ex);
}
}
public static void ensureTickThread(final Entity entity, final String reason) {
if (!isTickThreadFor(entity)) {
LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
throw new IllegalStateException(reason);
final String ex = "Thread failed main thread check: " +
reason + ", context=" + getThreadContext() + ", entity=" + EntityUtil.dumpEntity(entity);
LOGGER.error(ex, new Throwable());
throw new IllegalStateException(ex);
}
}
public static void ensureTickThread(final Level world, final AABB aabb, final String reason) {
if (!isTickThreadFor(world, aabb)) {
LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
throw new IllegalStateException(reason);
final String ex = "Thread failed main thread check: " +
reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", aabb=" + aabb;
LOGGER.error(ex, new Throwable());
throw new IllegalStateException(ex);
}
}
public static void ensureTickThread(final Level world, final double blockX, final double blockZ, final String reason) {
if (!isTickThreadFor(world, blockX, blockZ)) {
LOGGER.error("Thread " + Thread.currentThread().getName() + " failed main thread check: " + reason, new Throwable());
throw new IllegalStateException(reason);
final String ex = "Thread failed main thread check: " +
reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + new Vec3(blockX, 0.0, blockZ);
LOGGER.error(ex, new Throwable());
throw new IllegalStateException(ex);
}
}
@@ -105,6 +130,10 @@ public class TickThread extends Thread {
return isTickThread();
}
public static boolean isTickThreadFor(final Level world, final BlockPos pos, final int blockRadius) {
return isTickThread();
}
public static boolean isTickThreadFor(final Level world, final ChunkPos pos) {
return isTickThread();
}

View File

@@ -0,0 +1,25 @@
package ca.spottedleaf.moonrise.mixin.chunk_system;
import net.minecraft.core.SectionPos;
import net.minecraft.world.level.gameevent.DynamicGameEventListener;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(DynamicGameEventListener.class)
abstract class DynamicGameEventListenerMixin {
@Shadow @Nullable private SectionPos lastSection;
@Inject(method = "remove", at = @At("RETURN"))
private void onRemove(final CallbackInfo ci) {
// We need to unset the last section when removed, otherwise if the same instance is re-added at the same position it
// will assume there was no change and fail to re-register.
// In vanilla, chunks rarely unload and re-load quickly enough to trigger this issue. However, our chunk system has a
// quirk where fast chunk reload cycles will often occur on player login (see PR #22).
// So we fix this vanilla oversight as our changes cause it to manifest in bugs much more often (see issue #87).
this.lastSection = null;
}
}

View File

@@ -74,7 +74,7 @@ interface EntityGetterMixin {
@Overwrite
default boolean isUnobstructed(final Entity entity, final VoxelShape voxel) {
if (voxel.isEmpty()) {
return false;
return true;
}
final AABB singleAABB = ((CollisionVoxelShape)voxel).moonrise$getSingleAABBRepresentation();

View File

@@ -55,7 +55,7 @@ abstract class LevelMixin implements LevelAccessor, AutoCloseable {
public boolean isUnobstructed(final Entity entity) {
final AABB boundingBox = entity.getBoundingBox();
if (CollisionUtil.isEmpty(boundingBox)) {
return false;
return true;
}
final List<Entity> entities = this.getEntities(

View File

@@ -1,10 +1,13 @@
package ca.spottedleaf.moonrise.mixin.serverlist;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.multiplayer.resolver.ServerAddress;
import net.minecraft.client.multiplayer.resolver.ServerAddressResolver;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
@Mixin(ServerAddressResolver.class)
@@ -15,24 +18,23 @@ interface ServerAddressResolverMixin {
* @author Spottedleaf
*/
@Redirect(
method = {
"method_36903",
"*(Lnet/minecraft/client/multiplayer/resolver/ServerAddress;)Ljava/util/Optional;"
},
at = @At(
value = "INVOKE",
target = "Ljava/net/InetAddress;getByName(Ljava/lang/String;)Ljava/net/InetAddress;"
)
method = {
"method_36903",
"lambda$static$0"
},
at = @At(
value = "NEW",
target = "(Ljava/net/InetAddress;I)Ljava/net/InetSocketAddress;"
)
)
private static InetAddress eliminateRDNS(final String name) throws UnknownHostException {
final InetAddress ret = InetAddress.getByName(name);
final byte[] address = ret.getAddress();
private static InetSocketAddress eliminateRDNS(InetAddress addr, final int port,
@Local(ordinal = 0, argsOnly = true) final ServerAddress serverAddress) throws UnknownHostException {
final byte[] address = addr.getAddress();
if (address != null) {
// pass name to prevent rDNS
return InetAddress.getByAddress(name, address);
addr = InetAddress.getByAddress(serverAddress.getHost(), address);
}
return ret;
return new InetSocketAddress(addr, port);
}
}

View File

@@ -1143,7 +1143,7 @@ public final class MoonriseRegionFileIO {
LOGGER.error("Failed to decompress chunk data for task: " + this.toString(), thr);
}
if (compoundTag == null) {
if (throwable == null && compoundTag == null) {
// need to re-try from the start
this.scheduleReadIO();
return;

View File

@@ -46,7 +46,7 @@ import java.util.function.Function;
public final class RegionizedPlayerChunkLoader {
public static final TicketType<Long> PLAYER_TICKET = TicketType.create("chunk_system:player_ticket", Long::compareTo);
public static final TicketType<Long> PLAYER_TICKET_DELAYED = TicketType.create("chunk_system:player_ticket_delayed", Long::compareTo, 5 * 20);
public static final TicketType<Long> PLAYER_TICKET_DELAYED = TicketType.create("chunk_system:player_ticket_delayed", Long::compareTo, 1);
public static final int MIN_VIEW_DISTANCE = 2;
public static final int MAX_VIEW_DISTANCE = 32;
@@ -55,6 +55,10 @@ public final class RegionizedPlayerChunkLoader {
public static final int LOADED_TICKET_LEVEL = ChunkTaskScheduler.getTicketLevel(ChunkStatus.EMPTY);
public static final int TICK_TICKET_LEVEL = ChunkHolderManager.ENTITY_TICKING_TICKET_LEVEL;
public static void setUnloadDelay(final long ticks) {
PLAYER_TICKET_DELAYED.timeout = Math.max(1, ticks);
}
public static final class ViewDistanceHolder {
private volatile ViewDistances viewDistances;

View File

@@ -1,5 +1,6 @@
package ca.spottedleaf.moonrise.patches.chunk_system.scheduling;
import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
import ca.spottedleaf.concurrentutil.lock.ReentrantAreaLock;
import ca.spottedleaf.concurrentutil.map.ConcurrentLong2ReferenceChainedHashTable;
import ca.spottedleaf.concurrentutil.util.Priority;
@@ -81,6 +82,7 @@ public final class ChunkHolderManager {
private long currentTick;
private final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = new ArrayDeque<>();
private final MultiThreadedQueue<NewChunkHolder> offThreadPendingFullLoadUpdate = new MultiThreadedQueue<>();
private final ObjectRBTreeSet<NewChunkHolder> autoSaveQueue = new ObjectRBTreeSet<>((final NewChunkHolder c1, final NewChunkHolder c2) -> {
if (c1 == c2) {
return 0;
@@ -109,20 +111,20 @@ public final class ChunkHolderManager {
this.unloadQueue = new ChunkUnloadQueue(((ChunkSystemServerLevel)world).moonrise$getRegionChunkShift());
}
public boolean processTicketUpdates(final int posX, final int posZ) {
public boolean processTicketUpdates(final int chunkX, final int chunkZ) {
final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT;
final int ticketMask = (1 << ticketShift) - 1;
final List<ChunkProgressionTask> scheduledTasks = new ArrayList<>();
final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
final boolean ret;
final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
((posX >> ticketShift) - 1) << ticketShift,
((posZ >> ticketShift) - 1) << ticketShift,
(((posX >> ticketShift) + 1) << ticketShift) | ticketMask,
(((posZ >> ticketShift) + 1) << ticketShift) | ticketMask
((chunkX >> ticketShift) - 1) << ticketShift,
((chunkZ >> ticketShift) - 1) << ticketShift,
(((chunkX >> ticketShift) + 1) << ticketShift) | ticketMask,
(((chunkZ >> ticketShift) + 1) << ticketShift) | ticketMask
);
try {
ret = this.processTicketUpdatesNoLock(posX >> ticketShift, posZ >> ticketShift, scheduledTasks, changedFullStatus);
ret = this.processTicketUpdatesNoLock(chunkX >> ticketShift, chunkZ >> ticketShift, scheduledTasks, changedFullStatus);
} finally {
this.ticketLockArea.unlock(ticketLock);
}
@@ -720,6 +722,9 @@ public final class ChunkHolderManager {
return removeDelay <= 0L;
};
final List<ChunkProgressionTask> scheduledTasks = new ArrayList<>();
final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
for (final PrimitiveIterator.OfLong iterator = this.sectionToChunkToExpireCount.keyIterator(); iterator.hasNext();) {
final long sectionKey = iterator.nextLong();
@@ -728,9 +733,16 @@ public final class ChunkHolderManager {
continue;
}
final int lowerChunkX = CoordinateUtils.getChunkX(sectionKey) << sectionShift;
final int lowerChunkZ = CoordinateUtils.getChunkZ(sectionKey) << sectionShift;
final int ticketShift = ThreadedTicketLevelPropagator.SECTION_SHIFT;
final int ticketMask = (1 << ticketShift) - 1;
final ReentrantAreaLock.Node ticketLock = this.ticketLockArea.lock(
CoordinateUtils.getChunkX(sectionKey) << sectionShift,
CoordinateUtils.getChunkZ(sectionKey) << sectionShift
((lowerChunkX >> ticketShift) - 1) << ticketShift,
((lowerChunkZ >> ticketShift) - 1) << ticketShift,
(((lowerChunkX >> ticketShift) + 1) << ticketShift) | ticketMask,
(((lowerChunkZ >> ticketShift) + 1) << ticketShift) | ticketMask
);
try {
@@ -777,9 +789,23 @@ public final class ChunkHolderManager {
if (chunkToExpireCount.isEmpty()) {
this.sectionToChunkToExpireCount.remove(sectionKey);
}
// In order to prevent a race condition where an off-thread invokes processTicketUpdates(), we need to process ticket updates here
// so that we catch any additions to the changed full status list. If an off-thread were to process tickets here, it would not be guaranteed
// that it would be added to the full changed status set by the end of the call - possibly allowing ticket level decreases to be processed
// outside of this call, which is not an intended or expected of this chunk system.
this.processTicketUpdatesNoLock(lowerChunkX >> ThreadedTicketLevelPropagator.SECTION_SHIFT, lowerChunkZ >> ThreadedTicketLevelPropagator.SECTION_SHIFT, scheduledTasks, changedFullStatus);
} finally {
this.ticketLockArea.unlock(ticketLock);
}
this.addChangedStatuses(changedFullStatus);
changedFullStatus.clear(); // clear for next loop iteration
for (int i = 0, len = scheduledTasks.size(); i < len; ++i) {
scheduledTasks.get(i).schedule();
}
scheduledTasks.clear(); // clear for next loop iteration
}
this.processTicketUpdates();
@@ -1006,14 +1032,9 @@ public final class ChunkHolderManager {
return;
}
if (!TickThread.isTickThread()) {
this.taskScheduler.scheduleChunkTask(() -> {
final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = ChunkHolderManager.this.pendingFullLoadUpdate;
for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
pendingFullLoadUpdate.add(changedFullStatus.get(i));
}
ChunkHolderManager.this.processPendingFullUpdate();
}, Priority.HIGHEST);
// These will be handled on the next ServerChunkCache$MainThreadExecutor#pollTask, as it runs the distance manager update
// which will invoke processTicketUpdates
this.offThreadPendingFullLoadUpdate.addAll(changedFullStatus);
} else {
final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
for (int i = 0, len = changedFullStatus.size(); i < len; ++i) {
@@ -1294,36 +1315,20 @@ public final class ChunkHolderManager {
}
public boolean processTicketUpdates() {
return this.processTicketUpdates(true, null);
}
private static final ThreadLocal<List<ChunkProgressionTask>> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>();
static List<ChunkProgressionTask> getCurrentTicketUpdateScheduling() {
return CURRENT_TICKET_UPDATE_SCHEDULING.get();
}
private boolean processTicketUpdates(final boolean processFullUpdates, List<ChunkProgressionTask> scheduledTasks) {
if (BLOCK_TICKET_UPDATES.get() == Boolean.TRUE) {
throw new IllegalStateException("Cannot update ticket level while unloading chunks or updating entity manager");
}
if (!PlatformHooks.get().allowAsyncTicketUpdates() && !TickThread.isTickThread()) {
final boolean isTickThread = TickThread.isTickThread();
if (!PlatformHooks.get().allowAsyncTicketUpdates() && isTickThread) {
TickThread.ensureTickThread("Cannot asynchronously process ticket updates");
}
List<NewChunkHolder> changedFullStatus = null;
final boolean isTickThread = TickThread.isTickThread();
boolean ret = false;
final boolean canProcessFullUpdates = processFullUpdates & isTickThread;
final boolean canProcessScheduling = scheduledTasks == null;
if (this.ticketLevelPropagator.hasPendingUpdates()) {
if (scheduledTasks == null) {
scheduledTasks = new ArrayList<>();
}
changedFullStatus = new ArrayList<>();
final List<ChunkProgressionTask> scheduledTasks = new ArrayList<>();
final List<NewChunkHolder> changedFullStatus = new ArrayList<>();
this.blockTicketUpdates();
try {
@@ -1334,27 +1339,42 @@ public final class ChunkHolderManager {
} finally {
this.unblockTicketUpdates(Boolean.FALSE);
}
}
if (changedFullStatus != null) {
this.addChangedStatuses(changedFullStatus);
}
if (canProcessScheduling && scheduledTasks != null) {
for (int i = 0, len = scheduledTasks.size(); i < len; ++i) {
scheduledTasks.get(i).schedule();
}
}
if (canProcessFullUpdates) {
if (isTickThread) {
ret |= this.processPendingFullUpdate();
}
return ret;
}
private static final ThreadLocal<List<ChunkProgressionTask>> CURRENT_TICKET_UPDATE_SCHEDULING = new ThreadLocal<>();
static List<ChunkProgressionTask> getCurrentTicketUpdateScheduling() {
return CURRENT_TICKET_UPDATE_SCHEDULING.get();
}
// only call on tick thread
private void processOffThreadFullUpdates() {
final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
final MultiThreadedQueue<NewChunkHolder> offThreadPendingFullLoadUpdate = this.offThreadPendingFullLoadUpdate;
NewChunkHolder toUpdate;
while ((toUpdate = offThreadPendingFullLoadUpdate.poll()) != null) {
pendingFullLoadUpdate.add(toUpdate);
}
}
// only call on tick thread
private boolean processPendingFullUpdate() {
this.processOffThreadFullUpdates();
final ArrayDeque<NewChunkHolder> pendingFullLoadUpdate = this.pendingFullLoadUpdate;
boolean ret = false;

View File

@@ -220,6 +220,7 @@ accessible class net/minecraft/server/level/DistanceManager$FixedPlayerDistanceC
# Ticket
accessible field net/minecraft/server/level/Ticket key Ljava/lang/Object;
accessible field net/minecraft/server/level/TicketType timeout J
mutable field net/minecraft/server/level/TicketType timeout J
# ServerChunkCache

View File

@@ -26,6 +26,7 @@
"chunk_system.ChunkStepMixin",
"chunk_system.ChunkStorageMixin",
"chunk_system.DistanceManagerMixin",
"chunk_system.DynamicGameEventListenerMixin",
"chunk_system.EntityGetterMixin",
"chunk_system.EntityMixin",
"chunk_system.EntityTickListMixin",