9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2026-01-06 15:52:03 +00:00

Merge pull request #478 from Xiao-MoMi/dev

同步dev分支
This commit is contained in:
XiaoMoMi
2025-11-24 01:22:13 +08:00
committed by GitHub
85 changed files with 2287 additions and 342 deletions

View File

@@ -21,7 +21,7 @@ dependencies {
implementation("net.momirealms:sparrow-nbt-codec:${rootProject.properties["sparrow_nbt_version"]}")
implementation("net.momirealms:sparrow-nbt-legacy-codec:${rootProject.properties["sparrow_nbt_version"]}")
// S3
implementation("net.momirealms:craft-engine-s3:0.8")
implementation("net.momirealms:craft-engine-s3:0.9")
// Util
compileOnly("net.momirealms:sparrow-util:${rootProject.properties["sparrow_util_version"]}")
// Adventure
@@ -69,6 +69,8 @@ dependencies {
compileOnly("com.mojang:authlib:${rootProject.properties["authlib_version"]}")
// concurrentutil
compileOnly("ca.spottedleaf:concurrentutil:${rootProject.properties["concurrent_util_version"]}")
// bucket4j
compileOnly("com.bucket4j:bucket4j_jdk17-core:${rootProject.properties["bucket4j_version"]}")
}
java {
@@ -107,18 +109,28 @@ tasks {
relocate("io.netty.handler.codec.rtsp", "net.momirealms.craftengine.libraries.netty.handler.codec.rtsp")
relocate("io.netty.handler.codec.spdy", "net.momirealms.craftengine.libraries.netty.handler.codec.spdy")
relocate("io.netty.handler.codec.http2", "net.momirealms.craftengine.libraries.netty.handler.codec.http2")
relocate("io.github.bucket4j", "net.momirealms.craftengine.libraries.bucket4j") // bucket4j
}
}
publishing {
repositories {
maven {
name = "releases"
url = uri("https://repo.momirealms.net/releases")
credentials(PasswordCredentials::class) {
username = System.getenv("REPO_USERNAME")
password = System.getenv("REPO_PASSWORD")
}
}
maven {
name = "snapshot"
url = uri("https://repo.momirealms.net/snapshots")
credentials(PasswordCredentials::class) {
username = System.getenv("REPO_USERNAME")
password = System.getenv("REPO_PASSWORD")
}
}
}
publications {
create<MavenPublication>("mavenJava") {
@@ -139,5 +151,35 @@ publishing {
}
}
}
create<MavenPublication>("mavenJavaSnapshot") {
groupId = "net.momirealms"
artifactId = "craft-engine-core"
version = "${rootProject.properties["project_version"]}-SNAPSHOT"
artifact(tasks["sourcesJar"])
from(components["shadow"])
pom {
name = "CraftEngine API"
url = "https://github.com/Xiao-MoMi/craft-engine"
licenses {
license {
name = "GNU General Public License v3.0"
url = "https://www.gnu.org/licenses/gpl-3.0.html"
distribution = "repo"
}
}
}
}
}
}
tasks.register("publishRelease") {
group = "publishing"
description = "Publishes to the release repository"
dependsOn("publishMavenJavaPublicationToReleaseRepository")
}
tasks.register("publishSnapshot") {
group = "publishing"
description = "Publishes to the snapshot repository"
dependsOn("publishMavenJavaSnapshotPublicationToSnapshotRepository")
}

View File

@@ -550,7 +550,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
}
AutoStateGroup group = AutoStateGroup.byId(autoStateType);
if (group == null) {
throw new LocalizedResourceConfigException("warning.config.block.state.invalid_auto_state", autoStateId, EnumUtils.toString(AutoStateGroup.values()));
throw new LocalizedResourceConfigException("warning.config.block.state.invalid_auto_state", autoStateType, EnumUtils.toString(AutoStateGroup.values()));
}
futureVisualStates.put(appearanceName, this.visualBlockStateAllocator.requestAutoState(autoStateId, group));
} else {
@@ -704,7 +704,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
@NotNull
private Map<String, Property<?>> parseBlockProperties(Map<String, Object> propertiesSection) {
Map<String, Property<?>> properties = new HashMap<>();
Map<String, Property<?>> properties = new LinkedHashMap<>();
for (Map.Entry<String, Object> entry : propertiesSection.entrySet()) {
Property<?> property = Properties.fromMap(entry.getKey(), ResourceConfigUtils.getAsMap(entry.getValue(), entry.getKey()));
properties.put(entry.getKey(), property);

View File

@@ -10,21 +10,21 @@ import java.util.function.Predicate;
public enum AutoStateGroup {
NON_TINTABLE_LEAVES(List.of("no_tint_leaves", "leaves_no_tint", "non_tintable_leaves"),
Set.of(BlockKeys.SPRUCE_LEAVES, BlockKeys.CHERRY_LEAVES, BlockKeys.PALE_OAK_LEAVES, BlockKeys.AZALEA_LEAVES, BlockKeys.FLOWERING_AZALEA_LEAVES),
Set.of(BlockKeys.AZALEA_LEAVES, BlockKeys.FLOWERING_AZALEA_LEAVES, BlockKeys.CHERRY_LEAVES, BlockKeys.PALE_OAK_LEAVES),
(w) -> !(boolean) w.getProperty("waterlogged")
),
WATERLOGGED_NON_TINTABLE_LEAVES(
List.of("waterlogged_no_tint_leaves", "waterlogged_leaves_no_tint", "waterlogged_non_tintable_leaves"),
Set.of(BlockKeys.SPRUCE_LEAVES, BlockKeys.CHERRY_LEAVES, BlockKeys.PALE_OAK_LEAVES, BlockKeys.AZALEA_LEAVES, BlockKeys.FLOWERING_AZALEA_LEAVES),
Set.of(BlockKeys.AZALEA_LEAVES, BlockKeys.FLOWERING_AZALEA_LEAVES, BlockKeys.CHERRY_LEAVES, BlockKeys.PALE_OAK_LEAVES),
(w) -> w.getProperty("waterlogged")
),
TINTABLE_LEAVES("tintable_leaves",
Set.of(BlockKeys.OAK_LEAVES, BlockKeys.BIRCH_LEAVES, BlockKeys.JUNGLE_LEAVES, BlockKeys.ACACIA_LEAVES, BlockKeys.DARK_OAK_LEAVES, BlockKeys.MANGROVE_LEAVES),
Set.of(BlockKeys.OAK_LEAVES, BlockKeys.SPRUCE_LEAVES, BlockKeys.BIRCH_LEAVES, BlockKeys.JUNGLE_LEAVES, BlockKeys.ACACIA_LEAVES, BlockKeys.DARK_OAK_LEAVES, BlockKeys.MANGROVE_LEAVES),
(w) -> !(boolean) w.getProperty("waterlogged")
),
WATERLOGGED_TINTABLE_LEAVES(
"waterlogged_tintable_leaves",
Set.of(BlockKeys.OAK_LEAVES, BlockKeys.BIRCH_LEAVES, BlockKeys.JUNGLE_LEAVES, BlockKeys.ACACIA_LEAVES, BlockKeys.DARK_OAK_LEAVES, BlockKeys.MANGROVE_LEAVES),
Set.of(BlockKeys.OAK_LEAVES, BlockKeys.SPRUCE_LEAVES, BlockKeys.BIRCH_LEAVES, BlockKeys.JUNGLE_LEAVES, BlockKeys.ACACIA_LEAVES, BlockKeys.DARK_OAK_LEAVES, BlockKeys.MANGROVE_LEAVES),
(w) -> w.getProperty("waterlogged")
),
LEAVES("leaves",

View File

@@ -73,7 +73,7 @@ public abstract class BlockBehavior {
superMethod.call();
}
// 1.20+ BlockState state, LevelReader world, BlockPos pos
// BlockState state, LevelReader world, BlockPos pos
public boolean canSurvive(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
return (boolean) superMethod.call();
}

View File

@@ -0,0 +1,10 @@
package net.momirealms.craftengine.core.block.behavior;
import java.util.concurrent.Callable;
public interface IsPathFindableBlockBehavior {
// 1.20-1.20.4 BlockState state, BlockGetter world, BlockPos pos, PathComputationType type
// 1.20.5+ BlockState state, PathComputationType pathComputationType
boolean isPathFindable(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception;
}

View File

@@ -132,7 +132,7 @@ public abstract class AbstractFurnitureManager implements FurnitureManager {
.item(Key.of(ResourceConfigUtils.requireNonEmptyStringOrThrow(element.get("item"), "warning.config.furniture.element.missing_item")))
.applyDyedColor(ResourceConfigUtils.getAsBoolean(element.getOrDefault("apply-dyed-color", true), "apply-dyed-color"))
.billboard(ResourceConfigUtils.getOrDefault(element.get("billboard"), o -> Billboard.valueOf(o.toString().toUpperCase(Locale.ENGLISH)), Billboard.FIXED))
.transform(ResourceConfigUtils.getOrDefault(element.get("transform"), o -> ItemDisplayContext.valueOf(o.toString().toUpperCase(Locale.ENGLISH)), ItemDisplayContext.NONE))
.transform(ResourceConfigUtils.getOrDefault(ResourceConfigUtils.get(element, "transform", "display-transform"), o -> ItemDisplayContext.valueOf(o.toString().toUpperCase(Locale.ENGLISH)), ItemDisplayContext.NONE))
.scale(ResourceConfigUtils.getAsVector3f(element.getOrDefault("scale", "1"), "scale"))
.position(ResourceConfigUtils.getAsVector3f(element.getOrDefault("position", "0"), "position"))
.translation(ResourceConfigUtils.getAsVector3f(element.getOrDefault("translation", "0"), "translation"))

View File

@@ -26,6 +26,9 @@ public abstract class Player extends AbstractEntity implements NetWorkUser {
@NotNull
public abstract Item<?> getItemInHand(InteractionHand hand);
@NotNull
public abstract Item<?> getItemBySlot(int slot);
@Override
public abstract Object platformPlayer();

View File

@@ -17,6 +17,7 @@ import net.momirealms.craftengine.core.plugin.text.component.ComponentProvider;
import net.momirealms.craftengine.core.util.*;
import org.ahocorasick.trie.Token;
import org.ahocorasick.trie.Trie;
import org.incendo.cloud.suggestion.Suggestion;
import org.jetbrains.annotations.NotNull;
import javax.imageio.ImageIO;
@@ -52,6 +53,8 @@ public abstract class AbstractFontManager implements FontManager {
protected Map<String, Emoji> emojiMapper;
protected List<Emoji> emojiList;
protected List<String> allEmojiSuggestions;
// Cached command suggestions
protected final List<Suggestion> cachedImagesSuggestions = new ArrayList<>();
public AbstractFontManager(CraftEngine plugin) {
this.plugin = plugin;
@@ -95,6 +98,7 @@ public abstract class AbstractFontManager implements FontManager {
public void unload() {
this.fonts.clear();
this.images.clear();
this.cachedImagesSuggestions.clear();
this.illegalChars.clear();
this.emojis.clear();
this.networkTagTrie = null;
@@ -415,6 +419,12 @@ public abstract class AbstractFontManager implements FontManager {
return Optional.ofNullable(this.fonts.get(id));
}
@Override
public Collection<Suggestion> cachedImagesSuggestions() {
return Collections.unmodifiableCollection(this.cachedImagesSuggestions);
}
private Font getOrCreateFont(Key key) {
return this.fonts.computeIfAbsent(key, Font::new);
}
@@ -712,6 +722,7 @@ public abstract class AbstractFontManager implements FontManager {
}
AbstractFontManager.this.images.put(id, bitmapImage);
AbstractFontManager.this.cachedImagesSuggestions.add(Suggestion.suggestion(id.asString()));
}, () -> GsonHelper.get().toJson(section)));
}

View File

@@ -8,6 +8,7 @@ import net.momirealms.craftengine.core.plugin.config.ConfigParser;
import net.momirealms.craftengine.core.plugin.text.component.ComponentProvider;
import net.momirealms.craftengine.core.util.*;
import net.momirealms.sparrow.nbt.Tag;
import org.incendo.cloud.suggestion.Suggestion;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -80,6 +81,8 @@ public interface FontManager extends Manageable {
Optional<Font> fontById(Key font);
Collection<Suggestion> cachedImagesSuggestions();
int codepointByImageId(Key imageId, int x, int y);
default int codepointByImageId(Key imageId) {

View File

@@ -113,7 +113,7 @@ public interface ItemManager<T> extends Manageable, ModelGenerator {
Optional<Item<T>> c2s(Item<T> item);
Optional<Item<T>> s2c(Item<T> item, Player player);
Optional<Item<T>> s2c(Item<T> item, @Nullable Player player);
UniqueIdItem<T> uniqueEmptyItem();

View File

@@ -47,6 +47,7 @@ public class ItemSettings {
Color dyeColor;
@Nullable
Color fireworkColor;
Map<CustomDataType<?>, Object> customData = new IdentityHashMap<>(4);
private ItemSettings() {}
@@ -108,6 +109,7 @@ public class ItemSettings {
newSettings.dyeColor = settings.dyeColor;
newSettings.fireworkColor = settings.fireworkColor;
newSettings.ingredientSubstitutes = settings.ingredientSubstitutes;
newSettings.customData = settings.customData;
return newSettings;
}
@@ -123,73 +125,86 @@ public class ItemSettings {
return settings;
}
@SuppressWarnings("unchecked")
public <T> T getCustomData(CustomDataType<T> type) {
return (T) this.customData.get(type);
}
public void clearCustomData() {
this.customData.clear();
}
public <T> void addCustomData(CustomDataType<T> key, T value) {
this.customData.put(key, value);
}
public ProjectileMeta projectileMeta() {
return projectileMeta;
return this.projectileMeta;
}
public boolean disableVanillaBehavior() {
return disableVanillaBehavior;
return this.disableVanillaBehavior;
}
public Repairable repairable() {
return repairable;
return this.repairable;
}
public int fuelTime() {
return fuelTime;
return this.fuelTime;
}
public boolean renameable() {
return renameable;
return this.renameable;
}
public Set<Key> tags() {
return tags;
return this.tags;
}
public Tristate dyeable() {
return dyeable;
return this.dyeable;
}
public boolean canEnchant() {
return canEnchant;
return this.canEnchant;
}
public List<AnvilRepairItem> repairItems() {
return anvilRepairItems;
return this.anvilRepairItems;
}
public boolean respectRepairableComponent() {
return respectRepairableComponent;
return this.respectRepairableComponent;
}
public List<Key> ingredientSubstitutes() {
return ingredientSubstitutes;
return this.ingredientSubstitutes;
}
@Nullable
public FoodData foodData() {
return foodData;
return this.foodData;
}
@Nullable
public Key consumeReplacement() {
return consumeReplacement;
return this.consumeReplacement;
}
@Nullable
public CraftRemainder craftRemainder() {
return craftRemainder;
return this.craftRemainder;
}
@Nullable
public Helmet helmet() {
return helmet;
return this.helmet;
}
@Nullable
public ItemEquipment equipment() {
return equipment;
return this.equipment;
}
@Nullable
@@ -203,11 +218,11 @@ public class ItemSettings {
}
public List<DamageSource> invulnerable() {
return invulnerable;
return this.invulnerable;
}
public float compostProbability() {
return compostProbability;
return this.compostProbability;
}
public ItemSettings fireworkColor(Color color) {
@@ -384,6 +399,9 @@ public class ItemSettings {
registerFactory("equippable", (value -> {
Map<String, Object> args = MiscUtils.castToMap(value, false);
EquipmentData data = EquipmentData.fromMap(args);
if (data.assetId() == null) {
throw new IllegalArgumentException("Please move 'equippable' option to 'data' section.");
}
ComponentBasedEquipment componentBasedEquipment = ComponentBasedEquipment.FACTORY.create(data.assetId(), args);
((AbstractItemManager<?>) CraftEngine.instance().itemManager()).addOrMergeEquipment(componentBasedEquipment);
ItemEquipment itemEquipment = new ItemEquipment(Tristate.FALSE, data, componentBasedEquipment);
@@ -482,7 +500,7 @@ public class ItemSettings {
registerFactory("ingredient-substitute", (value -> settings -> settings.ingredientSubstitutes(MiscUtils.getAsStringList(value).stream().map(Key::of).toList())));
}
private static void registerFactory(String id, ItemSettings.Modifier.Factory factory) {
public static void registerFactory(String id, ItemSettings.Modifier.Factory factory) {
FACTORIES.put(id, factory);
}
}

View File

@@ -18,7 +18,7 @@ public interface NetworkItemHandler<T> {
String NETWORK_OPERATION = "type";
String NETWORK_VALUE = "value";
Optional<Item<T>> s2c(Item<T> itemStack, Player player);
Optional<Item<T>> s2c(Item<T> itemStack, @Nullable Player player);
Optional<Item<T>> c2s(Item<T> itemStack);

View File

@@ -2,15 +2,17 @@ package net.momirealms.craftengine.core.item.equipment;
import net.momirealms.craftengine.core.util.Key;
import java.util.Objects;
public abstract class AbstractEquipment implements Equipment {
protected final Key assetId;
protected AbstractEquipment(Key assetId) {
this.assetId = assetId;
this.assetId = Objects.requireNonNull(assetId, "asset-id cannot be null");
}
@Override
public Key assetId() {
return assetId;
return this.assetId;
}
}

View File

@@ -143,5 +143,21 @@ public class ComponentBasedEquipment extends AbstractEquipment implements Suppli
return dyeData;
}
}
@Override
public @NotNull String toString() {
return "Layer{" +
"texture='" + texture + '\'' +
", data=" + data +
", usePlayerTexture=" + usePlayerTexture +
'}';
}
}
@Override
public String toString() {
return "ComponentBasedEquipment{" +
"layers=" + this.layers +
'}';
}
}

View File

@@ -0,0 +1,43 @@
package net.momirealms.craftengine.core.loot.entry;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.loot.LootConditions;
import net.momirealms.craftengine.core.loot.LootContext;
import net.momirealms.craftengine.core.plugin.context.Condition;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.ResourceConfigUtils;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
public class EmptyLoopEntryContainer<T> extends AbstractSingleLootEntryContainer<T> {
public static final Factory<?> FACTORY = new Factory<>();
protected EmptyLoopEntryContainer(List<Condition<LootContext>> conditions, int weight, int quality) {
super(conditions, null, weight, quality);
}
@Override
protected void createItem(Consumer<Item<T>> lootConsumer, LootContext context) {}
@Override
public Key type() {
return LootEntryContainers.EMPTY;
}
public static class Factory<A> implements LootEntryContainerFactory<A> {
@SuppressWarnings("unchecked")
@Override
public LootEntryContainer<A> create(Map<String, Object> arguments) {
int weight = ResourceConfigUtils.getAsInt(arguments.getOrDefault("weight", 1), "weight");
int quality = ResourceConfigUtils.getAsInt(arguments.getOrDefault("quality", 0), "quality");
List<Condition<LootContext>> conditions = Optional.ofNullable(arguments.get("conditions"))
.map(it -> LootConditions.fromMapList((List<Map<String, Object>>) it))
.orElse(Collections.emptyList());
return new EmptyLoopEntryContainer<>(conditions, weight, quality);
}
}
}

View File

@@ -18,6 +18,7 @@ public class LootEntryContainers {
public static final Key ITEM = Key.from("craftengine:item");
public static final Key FURNITURE_ITEM = Key.from("craftengine:furniture_item");
public static final Key EXP = Key.from("craftengine:exp");
public static final Key EMPTY = Key.from("craftengine:empty");
static {
register(ALTERNATIVES, AlternativesLootEntryContainer.FACTORY);
@@ -25,6 +26,7 @@ public class LootEntryContainers {
register(ITEM, SingleItemLootEntryContainer.FACTORY);
register(EXP, ExpLootEntryContainer.FACTORY);
register(FURNITURE_ITEM, FurnitureItemLootEntryContainer.FACTORY);
register(EMPTY, EmptyLoopEntryContainer.FACTORY);
}
public static <T> void register(Key key, LootEntryContainerFactory<T> factory) {

View File

@@ -35,6 +35,7 @@ import net.momirealms.craftengine.core.plugin.locale.LangData;
import net.momirealms.craftengine.core.plugin.locale.LocalizedException;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.plugin.locale.TranslationManager;
import net.momirealms.craftengine.core.plugin.logger.Debugger;
import net.momirealms.craftengine.core.sound.AbstractSoundManager;
import net.momirealms.craftengine.core.sound.SoundEvent;
import net.momirealms.craftengine.core.util.*;
@@ -1122,7 +1123,7 @@ public abstract class AbstractPackManager implements PackManager {
futures.add(CompletableFuture.runAsync(() -> {
try {
byte[] previousImageBytes = Files.readAllBytes(imagePath);
byte[] optimized = optimizeImage(previousImageBytes);
byte[] optimized = optimizeImage(imagePath, previousImageBytes);
previousBytes.addAndGet(previousImageBytes.length);
if (optimized.length < previousImageBytes.length) {
afterBytes.addAndGet(optimized.length);
@@ -1190,9 +1191,13 @@ public abstract class AbstractPackManager implements PackManager {
}
}
private byte[] optimizeImage(byte[] previousImageBytes) throws IOException {
private byte[] optimizeImage(Path imagePath, byte[] previousImageBytes) throws IOException {
try (ByteArrayInputStream is = new ByteArrayInputStream(previousImageBytes)) {
BufferedImage src = ImageIO.read(is);
if (src == null) {
Debugger.RESOURCE_PACK.debug(() -> "Cannot read image " + imagePath.toString());
return previousImageBytes;
}
if (src.getType() == BufferedImage.TYPE_CUSTOM) {
return previousImageBytes;
}
@@ -1539,7 +1544,14 @@ public abstract class AbstractPackManager implements PackManager {
}
}
if (Config.fixTextureAtlas()) {
texturesToFix.add(key);
String imagePath = "assets/" + key.namespace() + "/textures/" + key.value() + ".png";
for (Path rootPath : rootPaths) {
if (Files.exists(rootPath.resolve(imagePath))) {
texturesToFix.add(key);
continue label;
}
}
TranslationManager.instance().log("warning.config.resource_pack.generation.missing_model_texture", entry.getValue().stream().distinct().toList().toString(), imagePath);
} else {
TranslationManager.instance().log("warning.config.resource_pack.generation.texture_not_in_atlas", key.toString());
}
@@ -1903,6 +1915,11 @@ public abstract class AbstractPackManager implements PackManager {
private void processComponentBasedEquipment(ComponentBasedEquipment componentBasedEquipment, Path generatedPackPath) {
Key assetId = componentBasedEquipment.assetId();
if (assetId == null) {
this.plugin.logger().severe("Asset id is null for equipment " + componentBasedEquipment);
return;
}
if (Config.packMaxVersion().isAtOrAbove(MinecraftVersions.V1_21_4)) {
Path equipmentPath = generatedPackPath
.resolve("assets")

View File

@@ -1,5 +1,6 @@
package net.momirealms.craftengine.core.pack.host.impl;
import io.github.bucket4j.Bandwidth;
import net.momirealms.craftengine.core.pack.host.ResourcePackDownloadData;
import net.momirealms.craftengine.core.pack.host.ResourcePackHost;
import net.momirealms.craftengine.core.pack.host.ResourcePackHostFactory;
@@ -8,10 +9,10 @@ import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.locale.LocalizedException;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.util.ResourceConfigUtils;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -76,14 +77,28 @@ public class SelfHost implements ResourcePackHost {
boolean oneTimeToken = ResourceConfigUtils.getAsBoolean(arguments.getOrDefault("one-time-token", true), "one-time-token");
String protocol = arguments.getOrDefault("protocol", "http").toString();
boolean denyNonMinecraftRequest = ResourceConfigUtils.getAsBoolean(arguments.getOrDefault("deny-non-minecraft-request", true), "deny-non-minecraft-request");
Map<String, Object> rateMap = MiscUtils.castToMap(arguments.get("rate-map"), true);
int maxRequests = 5;
int resetInterval = 20_000;
if (rateMap != null) {
maxRequests = ResourceConfigUtils.getAsInt(rateMap.getOrDefault("max-requests", 5), "max-requests");
resetInterval = ResourceConfigUtils.getAsInt(rateMap.getOrDefault("reset-interval", 20), "reset-interval") * 1000;
Bandwidth limit = null;
Map<String, Object> rateLimitingSection = ResourceConfigUtils.getAsMapOrNull(arguments.get("rate-limiting"), "rate-limiting");
long maxBandwidthUsage = 0L;
long minDownloadSpeed = 50_000L;
if (rateLimitingSection != null) {
if (rateLimitingSection.containsKey("qps-per-ip")) {
String qps = rateLimitingSection.get("qps-per-ip").toString();
String[] split = qps.split("/", 2);
if (split.length == 1) split = new String[]{split[0], "1"};
int maxRequests = ResourceConfigUtils.getAsInt(split[0], "qps-per-ip");
int resetInterval = ResourceConfigUtils.getAsInt(split[1], "qps-per-ip");
limit = Bandwidth.builder()
.capacity(maxRequests)
.refillGreedy(maxRequests, Duration.ofSeconds(resetInterval))
.build();
}
maxBandwidthUsage = ResourceConfigUtils.getAsLong(rateLimitingSection.getOrDefault("max-bandwidth-per-second", 0), "max-bandwidth");
minDownloadSpeed = ResourceConfigUtils.getAsLong(rateLimitingSection.getOrDefault("min-download-speed-per-player", 50_000), "min-download-speed-per-player");
}
selfHostHttpServer.updateProperties(ip, port, url, denyNonMinecraftRequest, protocol, maxRequests, resetInterval, oneTimeToken);
selfHostHttpServer.updateProperties(ip, port, url, denyNonMinecraftRequest, protocol, limit, oneTimeToken, maxBandwidthUsage, minDownloadSpeed);
return INSTANCE;
}
}

View File

@@ -3,18 +3,27 @@ package net.momirealms.craftengine.core.pack.host.impl;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Scheduler;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.stream.ChunkedStream;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.traffic.GlobalChannelTrafficShapingHandler;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.GlobalEventExecutor;
import net.momirealms.craftengine.core.pack.host.ResourcePackDownloadData;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import org.jetbrains.annotations.Nullable;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
@@ -23,28 +32,35 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
public class SelfHostHttpServer {
private static SelfHostHttpServer instance;
private final Cache<String, Boolean> oneTimePackUrls = Caffeine.newBuilder()
.maximumSize(256)
.maximumSize(1024)
.scheduler(Scheduler.systemScheduler())
.expireAfterWrite(1, TimeUnit.MINUTES)
.build();
private final Cache<String, IpAccessRecord> ipAccessCache = Caffeine.newBuilder()
.maximumSize(256)
private final Cache<String, Bucket> ipRateLimiters = Caffeine.newBuilder()
.maximumSize(1024)
.scheduler(Scheduler.systemScheduler())
.expireAfterWrite(10, TimeUnit.MINUTES)
.expireAfterAccess(5, TimeUnit.MINUTES)
.build();
private final AtomicLong totalRequests = new AtomicLong();
private final AtomicLong blockedRequests = new AtomicLong();
private int rateLimit = 1;
private long rateLimitInterval = 1000;
private Bandwidth limitPerIp = Bandwidth.builder()
.capacity(1)
.refillGreedy(1, Duration.ofSeconds(1))
.initialTokens(1)
.build();
private String ip = "localhost";
private int port = -1;
private String protocol = "http";
@@ -52,6 +68,12 @@ public class SelfHostHttpServer {
private boolean denyNonMinecraft = true;
private boolean useToken;
private long globalUploadRateLimit = 0;
private long minDownloadSpeed = 50_000;
private GlobalChannelTrafficShapingHandler trafficShapingHandler;
private ScheduledExecutorService virtualTrafficExecutor;
private final ChannelGroup activeDownloadChannels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
private byte[] resourcePackBytes;
private String packHash;
private UUID packUUID;
@@ -72,17 +94,25 @@ public class SelfHostHttpServer {
String url,
boolean denyNonMinecraft,
String protocol,
int maxRequests,
int resetInterval,
boolean token) {
Bandwidth limitPerIp,
boolean token,
long globalUploadRateLimit,
long minDownloadSpeed) {
this.ip = ip;
this.url = url;
this.denyNonMinecraft = denyNonMinecraft;
this.protocol = protocol;
this.rateLimit = maxRequests;
this.rateLimitInterval = resetInterval;
this.limitPerIp = limitPerIp;
this.useToken = token;
if (this.globalUploadRateLimit != globalUploadRateLimit || this.minDownloadSpeed != minDownloadSpeed) {
this.globalUploadRateLimit = globalUploadRateLimit;
this.minDownloadSpeed = minDownloadSpeed;
if (this.trafficShapingHandler != null) {
long initSize = globalUploadRateLimit <= 0 ? 0 : Math.max(minDownloadSpeed, globalUploadRateLimit);
this.trafficShapingHandler.setWriteLimit(initSize);
this.trafficShapingHandler.setWriteChannelLimit(initSize);
}
}
if (port <= 0 || port > 65535) {
throw new IllegalArgumentException("Invalid port: " + port);
}
@@ -104,7 +134,17 @@ public class SelfHostHttpServer {
private void initializeServer() {
bossGroup = new NioEventLoopGroup(1);
workerGroup = new NioEventLoopGroup();
virtualTrafficExecutor = Executors.newScheduledThreadPool(1, Thread.ofVirtual().factory());
long initSize = globalUploadRateLimit <= 0 ? 0 : Math.max(minDownloadSpeed, globalUploadRateLimit);
trafficShapingHandler = new GlobalChannelTrafficShapingHandler(
virtualTrafficExecutor,
initSize,
0, // 全局读取不限
initSize, // 默认单通道和总体一致
0, // 单通道读取不限
100, // checkInterval (ms)
10_000 // maxTime (ms)
);
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
@@ -112,7 +152,9 @@ public class SelfHostHttpServer {
@Override
protected void initChannel(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("trafficShaping", trafficShapingHandler);
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new ChunkedWriteHandler());
pipeline.addLast(new HttpObjectAggregator(1048576));
pipeline.addLast(new RequestHandler());
}
@@ -128,6 +170,17 @@ public class SelfHostHttpServer {
@ChannelHandler.Sharable
private class RequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
super.channelInactive(ctx);
// 有人走了,其他人的速度上限提高
if (activeDownloadChannels.contains(ctx.channel())) {
activeDownloadChannels.remove(ctx.channel());
rebalanceBandwidth();
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
totalRequests.incrementAndGet();
@@ -136,7 +189,7 @@ public class SelfHostHttpServer {
String clientIp = ((InetSocketAddress) ctx.channel().remoteAddress())
.getAddress().getHostAddress();
if (checkRateLimit(clientIp)) {
if (!checkIpRateLimit(clientIp)) {
sendError(ctx, HttpResponseStatus.TOO_MANY_REQUESTS, "Rate limit exceeded");
blockedRequests.incrementAndGet();
return;
@@ -159,6 +212,7 @@ public class SelfHostHttpServer {
}
private void handleDownload(ChannelHandlerContext ctx, FullHttpRequest request, QueryStringDecoder queryDecoder) {
// 使用一次性token
if (useToken) {
String token = queryDecoder.parameters().getOrDefault("token", java.util.Collections.emptyList()).stream().findFirst().orElse(null);
if (!validateToken(token)) {
@@ -168,6 +222,7 @@ public class SelfHostHttpServer {
}
}
// 不是Minecraft客户端
if (denyNonMinecraft) {
String userAgent = request.headers().get(HttpHeaderNames.USER_AGENT);
if (userAgent == null || !userAgent.startsWith("Minecraft Java/")) {
@@ -177,22 +232,47 @@ public class SelfHostHttpServer {
}
}
// 没有资源包
if (resourcePackBytes == null) {
sendError(ctx, HttpResponseStatus.NOT_FOUND, "Resource pack missing");
blockedRequests.incrementAndGet();
return;
}
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,
Unpooled.wrappedBuffer(resourcePackBytes)
);
response.headers()
.set(HttpHeaderNames.CONTENT_TYPE, "application/zip")
.set(HttpHeaderNames.CONTENT_LENGTH, resourcePackBytes.length);
// 新人来了,所有人的速度上限降低
if (!activeDownloadChannels.contains(ctx.channel())) {
activeDownloadChannels.add(ctx.channel());
rebalanceBandwidth();
}
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
// 告诉客户端资源包大小
long fileLength = resourcePackBytes.length;
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
HttpUtil.setContentLength(response, fileLength);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/zip");
boolean keepAlive = HttpUtil.isKeepAlive(request);
if (keepAlive) {
response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
}
ctx.write(response);
// 发送分段资源包
ChunkedStream chunkedStream = new ChunkedStream(new ByteArrayInputStream(resourcePackBytes), 8192);
HttpChunkedInput httpChunkedInput = new HttpChunkedInput(chunkedStream);
ChannelFuture sendFileFuture = ctx.writeAndFlush(httpChunkedInput);
if (!keepAlive) {
sendFileFuture.addListener(ChannelFutureListener.CLOSE);
}
// 监听下载完成(成功或失败),以便在下载结束后(如果不关闭连接)也能移除计数
// 注意:如果是 Keep-Alive连接不会断但下载结束了。
// 为了精确控制,可以在这里监听 operationComplete
sendFileFuture.addListener((ChannelFutureListener) future -> {
if (activeDownloadChannels.contains(ctx.channel())) {
activeDownloadChannels.remove(ctx.channel());
rebalanceBandwidth();
}
});
}
private void handleMetrics(ChannelHandlerContext ctx) {
@@ -213,23 +293,11 @@ public class SelfHostHttpServer {
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
}
private boolean checkRateLimit(String clientIp) {
IpAccessRecord record = ipAccessCache.getIfPresent(clientIp);
long now = System.currentTimeMillis();
if (record == null) {
record = new IpAccessRecord(now, 1);
ipAccessCache.put(clientIp, record);
return false;
}
if (now - record.lastAccessTime > rateLimitInterval) {
record.lastAccessTime = now;
record.accessCount = 1;
return false;
}
return ++record.accessCount > rateLimit;
private boolean checkIpRateLimit(String clientIp) {
if (limitPerIp == null) return true;
Bucket rateLimiter = ipRateLimiters.get(clientIp, k -> Bucket.builder().addLimit(limitPerIp).build());
assert rateLimiter != null;
return rateLimiter.tryConsume(1);
}
private boolean validateToken(String token) {
@@ -257,6 +325,28 @@ public class SelfHostHttpServer {
}
}
private synchronized void rebalanceBandwidth() {
if (globalUploadRateLimit == 0) {
trafficShapingHandler.setWriteChannelLimit(0);
return;
}
int activeCount = activeDownloadChannels.size();
if (activeCount == 0) {
trafficShapingHandler.setWriteChannelLimit(globalUploadRateLimit);
return;
}
// 计算平均带宽:全局总量 / 当前人数
long fairRate = globalUploadRateLimit / activeCount;
// 确保不低于最小保障速率(可选,防止除法导致过小)
fairRate = Math.max(fairRate, this.minDownloadSpeed);
// 更新 Handler 配置
trafficShapingHandler.setWriteChannelLimit(fairRate);
}
@Nullable
public ResourcePackDownloadData generateOneTimeUrl() {
if (this.resourcePackBytes == null) return null;
@@ -275,6 +365,17 @@ public class SelfHostHttpServer {
}
public void disable() {
// 释放流量整形资源
if (trafficShapingHandler != null) {
trafficShapingHandler.release();
trafficShapingHandler = null;
}
// 关闭专用线程池
if (virtualTrafficExecutor != null) {
virtualTrafficExecutor.shutdown();
virtualTrafficExecutor = null;
}
activeDownloadChannels.close();
if (serverChannel != null) {
serverChannel.close().awaitUninterruptibly();
bossGroup.shutdownGracefully();
@@ -312,14 +413,4 @@ public class SelfHostHttpServer {
CraftEngine.instance().logger().severe("SHA-1 algorithm not available", e);
}
}
private static class IpAccessRecord {
long lastAccessTime;
int accessCount;
IpAccessRecord(long lastAccessTime, int accessCount) {
this.lastAccessTime = lastAccessTime;
this.accessCount = accessCount;
}
}
}

View File

@@ -267,8 +267,11 @@ public abstract class CraftEngine implements Plugin {
this.vanillaLootManager.delayedInit();
// 注册脱离坐骑监听器
this.seatManager.delayedInit();
// 注册世界加载相关监听器
this.worldManager.delayedInit();
if (!Config.delayConfigurationLoad()) {
// 注册世界加载相关监听器
this.worldManager.delayedInit();
}
// 延迟任务
this.beforeEnableTaskRegistry.executeTasks();
@@ -310,6 +313,7 @@ public abstract class CraftEngine implements Plugin {
} else {
try {
this.reloadPlugin(Runnable::run, Runnable::run, true);
this.worldManager.delayedInit();
} catch (Exception e) {
this.logger.severe("Failed to reload plugin on delayed enable stage", e);
}
@@ -415,7 +419,8 @@ public abstract class CraftEngine implements Plugin {
Dependencies.LZ4,
Dependencies.EVALEX,
Dependencies.NETTY_HTTP,
Dependencies.JIMFS
Dependencies.JIMFS,
Dependencies.BUCKET_4_J
);
}

View File

@@ -138,6 +138,8 @@ public class Config {
protected ColliderType furniture$collision_entity_type;
protected boolean block$sound_system$enable;
protected boolean block$sound_system$process_cancelled_events$step;
protected boolean block$sound_system$process_cancelled_events$break;
protected boolean block$simplify_adventure_break_check;
protected boolean block$simplify_adventure_place_check;
protected boolean block$predict_breaking;
@@ -475,6 +477,8 @@ public class Config {
// block
block$sound_system$enable = config.getBoolean("block.sound-system.enable", true);
block$sound_system$process_cancelled_events$step = config.getBoolean("block.sound-system.process-cancelled-events.step", true);
block$sound_system$process_cancelled_events$break = config.getBoolean("block.sound-system.process-cancelled-events.break", true);
block$simplify_adventure_break_check = config.getBoolean("block.simplify-adventure-break-check", false);
block$simplify_adventure_place_check = config.getBoolean("block.simplify-adventure-place-check", false);
block$predict_breaking = config.getBoolean("block.predict-breaking.enable", true);
@@ -675,6 +679,14 @@ public class Config {
return instance.block$sound_system$enable;
}
public static boolean processCancelledStep() {
return instance.block$sound_system$process_cancelled_events$step;
}
public static boolean processCancelledBreak() {
return instance.block$sound_system$process_cancelled_events$break;
}
public static boolean simplifyAdventureBreakCheck() {
return instance.block$simplify_adventure_break_check;
}

View File

@@ -52,11 +52,11 @@ public class TemplateManagerImpl implements TemplateManager {
@Override
public void parseObject(Pack pack, Path path, String node, Key id, Object obj) {
if (templates.containsKey(id)) {
if (TemplateManagerImpl.this.templates.containsKey(id)) {
throw new LocalizedResourceConfigException("warning.config.template.duplicate");
}
// 预处理会将 string类型的键或值解析为ArgumentString以加速模板应用。所以处理后不可能存在String类型。
templates.put(id, preprocessUnknownValue(obj));
TemplateManagerImpl.this.templates.put(id, preprocessUnknownValue(obj));
}
}

View File

@@ -5,6 +5,7 @@ import net.momirealms.craftengine.core.plugin.context.Condition;
import net.momirealms.craftengine.core.plugin.context.Context;
import net.momirealms.craftengine.core.plugin.context.parameter.DirectContextParameters;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.util.EnumUtils;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.ResourceConfigUtils;
@@ -42,7 +43,7 @@ public class HandCondition<CTX extends Context> implements Condition<CTX> {
try {
return new HandCondition<>(InteractionHand.valueOf(hand.toUpperCase(Locale.ENGLISH)));
} catch (IllegalArgumentException e) {
throw new LocalizedResourceConfigException("warning.config.condition.hand.invalid_hand", hand);
throw new LocalizedResourceConfigException("warning.config.condition.hand.invalid_hand", hand, EnumUtils.toString(InteractionHand.values()));
}
}
}

View File

@@ -1,5 +1,6 @@
package net.momirealms.craftengine.core.plugin.context.function;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.momirealms.craftengine.core.block.BlockStateWrapper;
import net.momirealms.craftengine.core.block.UpdateOption;
import net.momirealms.craftengine.core.plugin.context.Condition;
@@ -10,9 +11,9 @@ import net.momirealms.craftengine.core.plugin.context.parameter.DirectContextPar
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.util.ResourceConfigUtils;
import net.momirealms.craftengine.core.world.ExistingBlock;
import net.momirealms.craftengine.core.world.World;
import net.momirealms.craftengine.core.world.WorldPosition;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
@@ -20,15 +21,28 @@ import java.util.Optional;
public class CycleBlockPropertyFunction<CTX extends Context> extends AbstractConditionalFunction<CTX> {
private final String property;
@Nullable
private final Map<String, String> rules;
@Nullable
private final NumberProvider inverse;
private final NumberProvider x;
private final NumberProvider y;
private final NumberProvider z;
private final NumberProvider updateFlags;
public CycleBlockPropertyFunction(List<Condition<CTX>> predicates, String property, NumberProvider inverse, NumberProvider x, NumberProvider y, NumberProvider z, NumberProvider updateFlags) {
public CycleBlockPropertyFunction(
List<Condition<CTX>> predicates,
String property,
@Nullable Map<String, String> rules,
@Nullable NumberProvider inverse,
NumberProvider x,
NumberProvider y,
NumberProvider z,
NumberProvider updateFlags
) {
super(predicates);
this.property = property;
this.rules = rules;
this.inverse = inverse;
this.x = x;
this.y = y;
@@ -44,11 +58,26 @@ public class CycleBlockPropertyFunction<CTX extends Context> extends AbstractCon
int x = MiscUtils.fastFloor(this.x.getDouble(ctx));
int y = MiscUtils.fastFloor(this.y.getDouble(ctx));
int z = MiscUtils.fastFloor(this.z.getDouble(ctx));
ExistingBlock blockAt = world.getBlock(x, y, z);
BlockStateWrapper wrapper = blockAt.blockState().cycleProperty(this.property, this.inverse.getInt(ctx) == 0);
BlockStateWrapper wrapper = updateBlockState(world.getBlock(x, y, z).blockState(), ctx);
world.setBlockState(x, y, z, wrapper, this.updateFlags.getInt(ctx));
}
private BlockStateWrapper updateBlockState(BlockStateWrapper wrapper, CTX ctx) {
boolean inverse = this.inverse != null && this.inverse.getInt(ctx) == 0;
if (this.rules == null) {
return wrapper.cycleProperty(this.property, inverse);
}
Object value = wrapper.getProperty(this.property);
if (value == null) {
return wrapper.cycleProperty(this.property, inverse);
}
String mapValue = this.rules.get(value.toString());
if (mapValue == null) {
return wrapper.cycleProperty(this.property, inverse);
}
return wrapper.withProperty(this.property, mapValue);
}
@Override
public Key type() {
return CommonFunctions.CYCLE_BLOCK_PROPERTY;
@@ -62,8 +91,15 @@ public class CycleBlockPropertyFunction<CTX extends Context> extends AbstractCon
@Override
public Function<CTX> create(Map<String, Object> arguments) {
String property = ResourceConfigUtils.requireNonEmptyStringOrThrow(arguments.get("property"), "warning.config.function.cycle_block_property.missing_property");
Map<String, String> rules;
if (arguments.containsKey("rules")) {
rules = new Object2ObjectOpenHashMap<>();
MiscUtils.castToMap(arguments.get("rules"), false).forEach((k, v) -> rules.put(k, v.toString()));
} else rules = null;
return new CycleBlockPropertyFunction<>(getPredicates(arguments),
ResourceConfigUtils.requireNonEmptyStringOrThrow(arguments.get("property"), "warning.config.function.cycle_block_property.missing_property"),
property,
rules,
NumberProviders.fromObject(arguments.getOrDefault("inverse", "<arg:player.is_sneaking>")),
NumberProviders.fromObject(arguments.getOrDefault("x", "<arg:position.x>")),
NumberProviders.fromObject(arguments.getOrDefault("y", "<arg:position.y>")),

View File

@@ -372,6 +372,13 @@ public class Dependencies {
List.of(Relocation.of("jimfs", "com{}google{}common{}jimfs"))
);
public static final Dependency BUCKET_4_J = new Dependency(
"bucket4j",
"com{}bucket4j",
"bucket4j_jdk17-core",
List.of(Relocation.of("bucket4j", "io{}github{}bucket4j"))
);
public static final Dependency NETTY_HTTP = new Dependency(
"netty-codec-http",
"io{}netty",

View File

@@ -0,0 +1,453 @@
package net.momirealms.craftengine.core.plugin.entityculling;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.world.MutableVec3d;
import net.momirealms.craftengine.core.world.Vec3d;
import java.util.Arrays;
import java.util.BitSet;
public class EntityCulling {
// 面掩码常量
private static final int ON_MIN_X = 0x01;
private static final int ON_MAX_X = 0x02;
private static final int ON_MIN_Y = 0x04;
private static final int ON_MAX_Y = 0x08;
private static final int ON_MIN_Z = 0x10;
private static final int ON_MAX_Z = 0x20;
private final int reach;
private final double aabbExpansion;
private final DataProvider provider;
private final OcclusionCache cache;
// 重用数据结构减少GC压力
private final BitSet skipList = new BitSet();
private final MutableVec3d[] targetPoints = new MutableVec3d[15];
private final MutableVec3d targetPos = new MutableVec3d(0, 0, 0);
private final int[] cameraPos = new int[3];
private final boolean[] dotselectors = new boolean[14];
private final int[] lastHitBlock = new int[3];
// 状态标志
private boolean allowRayChecks = false;
private boolean allowWallClipping = false;
public EntityCulling(int maxDistance, DataProvider provider) {
this(maxDistance, provider, new ArrayOcclusionCache(maxDistance), 0.5);
}
public EntityCulling(int maxDistance, DataProvider provider, OcclusionCache cache, double aabbExpansion) {
this.reach = maxDistance;
this.provider = provider;
this.cache = cache;
this.aabbExpansion = aabbExpansion;
// 预先初始化点对象
for(int i = 0; i < targetPoints.length; i++) {
targetPoints[i] = new MutableVec3d(0, 0, 0);
}
}
public boolean isAABBVisible(Vec3d aabbMin, MutableVec3d aabbMax, MutableVec3d viewerPosition) {
try {
// 计算包围盒范围
int maxX = MiscUtils.fastFloor(aabbMax.x + aabbExpansion);
int maxY = MiscUtils.fastFloor(aabbMax.y + aabbExpansion);
int maxZ = MiscUtils.fastFloor(aabbMax.z + aabbExpansion);
int minX = MiscUtils.fastFloor(aabbMin.x - aabbExpansion);
int minY = MiscUtils.fastFloor(aabbMin.y - aabbExpansion);
int minZ = MiscUtils.fastFloor(aabbMin.z - aabbExpansion);
cameraPos[0] = MiscUtils.fastFloor(viewerPosition.x);
cameraPos[1] = MiscUtils.fastFloor(viewerPosition.y);
cameraPos[2] = MiscUtils.fastFloor(viewerPosition.z);
// 判断是否在包围盒内部
Relative relX = Relative.from(minX, maxX, cameraPos[0]);
Relative relY = Relative.from(minY, maxY, cameraPos[1]);
Relative relZ = Relative.from(minZ, maxZ, cameraPos[2]);
if(relX == Relative.INSIDE && relY == Relative.INSIDE && relZ == Relative.INSIDE) {
return true;
}
skipList.clear();
// 1. 快速检查缓存
int id = 0;
for (int x = minX; x <= maxX; x++) {
for (int y = minY; y <= maxY; y++) {
for (int z = minZ; z <= maxZ; z++) {
int cachedValue = getCacheValue(x, y, z);
if (cachedValue == 1) return true; // 缓存显示可见
if (cachedValue != 0) skipList.set(id); // 缓存显示不可见或遮挡
id++;
}
}
}
allowRayChecks = false;
id = 0;
// 2. 遍历体素进行光线投射检查
for (int x = minX; x <= maxX; x++) {
// 预计算X轴面的可见性和边缘数据
byte visibleOnFaceX = 0;
byte faceEdgeDataX = 0;
if (x == minX) { faceEdgeDataX |= ON_MIN_X; if (relX == Relative.POSITIVE) visibleOnFaceX |= ON_MIN_X; }
if (x == maxX) { faceEdgeDataX |= ON_MAX_X; if (relX == Relative.NEGATIVE) visibleOnFaceX |= ON_MAX_X; }
for (int y = minY; y <= maxY; y++) {
byte visibleOnFaceY = visibleOnFaceX;
byte faceEdgeDataY = faceEdgeDataX;
if (y == minY) { faceEdgeDataY |= ON_MIN_Y; if (relY == Relative.POSITIVE) visibleOnFaceY |= ON_MIN_Y; }
if (y == maxY) { faceEdgeDataY |= ON_MAX_Y; if (relY == Relative.NEGATIVE) visibleOnFaceY |= ON_MAX_Y; }
for (int z = minZ; z <= maxZ; z++) {
// 如果缓存已标记为不可见,跳过
if(skipList.get(id++)) continue;
byte visibleOnFace = visibleOnFaceY;
byte faceEdgeData = faceEdgeDataY;
if (z == minZ) { faceEdgeData |= ON_MIN_Z; if (relZ == Relative.POSITIVE) visibleOnFace |= ON_MIN_Z; }
if (z == maxZ) { faceEdgeData |= ON_MAX_Z; if (relZ == Relative.NEGATIVE) visibleOnFace |= ON_MAX_Z; }
if (visibleOnFace != 0) {
targetPos.set(x, y, z);
// 检查单个体素是否可见
if (isVoxelVisible(viewerPosition, targetPos, faceEdgeData, visibleOnFace)) {
return true;
}
}
}
}
}
return false;
} catch (Throwable t) {
t.printStackTrace();
return true; // 发生异常默认可见,防止渲染错误
}
}
// 接口定义
public interface DataProvider {
boolean prepareChunk(int chunkX, int chunkZ);
boolean isOpaqueFullCube(int x, int y, int z);
default void cleanup() {}
default void checkingPosition(MutableVec3d[] targetPoints, int size, MutableVec3d viewerPosition) {}
}
/**
* 检查单个体素是否对观察者可见
*/
private boolean isVoxelVisible(MutableVec3d viewerPosition, MutableVec3d position, byte faceData, byte visibleOnFace) {
int targetSize = 0;
Arrays.fill(dotselectors, false);
// 根据相对位置选择需要检测的关键点(角点和面中心点)
if((visibleOnFace & ON_MIN_X) != 0){
dotselectors[0] = true;
if((faceData & ~ON_MIN_X) != 0) { dotselectors[1] = dotselectors[4] = dotselectors[5] = true; }
dotselectors[8] = true;
}
if((visibleOnFace & ON_MIN_Y) != 0){
dotselectors[0] = true;
if((faceData & ~ON_MIN_Y) != 0) { dotselectors[3] = dotselectors[4] = dotselectors[7] = true; }
dotselectors[9] = true;
}
if((visibleOnFace & ON_MIN_Z) != 0){
dotselectors[0] = true;
if((faceData & ~ON_MIN_Z) != 0) { dotselectors[1] = dotselectors[4] = dotselectors[5] = true; }
dotselectors[10] = true;
}
if((visibleOnFace & ON_MAX_X) != 0){
dotselectors[4] = true;
if((faceData & ~ON_MAX_X) != 0) { dotselectors[5] = dotselectors[6] = dotselectors[7] = true; }
dotselectors[11] = true;
}
if((visibleOnFace & ON_MAX_Y) != 0){
dotselectors[1] = true;
if((faceData & ~ON_MAX_Y) != 0) { dotselectors[2] = dotselectors[5] = dotselectors[6] = true; }
dotselectors[12] = true;
}
if((visibleOnFace & ON_MAX_Z) != 0){
dotselectors[2] = true;
if((faceData & ~ON_MAX_Z) != 0) { dotselectors[3] = dotselectors[6] = dotselectors[7] = true; }
dotselectors[13] = true;
}
// 填充目标点使用偏移量防止Z-Fighting或精度问题
if (dotselectors[0]) targetPoints[targetSize++].add(position, 0.05, 0.05, 0.05);
if (dotselectors[1]) targetPoints[targetSize++].add(position, 0.05, 0.95, 0.05);
if (dotselectors[2]) targetPoints[targetSize++].add(position, 0.05, 0.95, 0.95);
if (dotselectors[3]) targetPoints[targetSize++].add(position, 0.05, 0.05, 0.95);
if (dotselectors[4]) targetPoints[targetSize++].add(position, 0.95, 0.05, 0.05);
if (dotselectors[5]) targetPoints[targetSize++].add(position, 0.95, 0.95, 0.05);
if (dotselectors[6]) targetPoints[targetSize++].add(position, 0.95, 0.95, 0.95);
if (dotselectors[7]) targetPoints[targetSize++].add(position, 0.95, 0.05, 0.95);
// 面中心点
if (dotselectors[8]) targetPoints[targetSize++].add(position, 0.05, 0.5, 0.5);
if (dotselectors[9]) targetPoints[targetSize++].add(position, 0.5, 0.05, 0.5);
if (dotselectors[10]) targetPoints[targetSize++].add(position, 0.5, 0.5, 0.05);
if (dotselectors[11]) targetPoints[targetSize++].add(position, 0.95, 0.5, 0.5);
if (dotselectors[12]) targetPoints[targetSize++].add(position, 0.5, 0.95, 0.5);
if (dotselectors[13]) targetPoints[targetSize++].add(position, 0.5, 0.5, 0.95);
return isVisible(viewerPosition, targetPoints, targetSize);
}
// 优化:使用基本数据类型代替对象分配
private boolean rayIntersection(int[] b, MutableVec3d rayOrigin, double dirX, double dirY, double dirZ) {
double invX = 1.0 / dirX;
double invY = 1.0 / dirY;
double invZ = 1.0 / dirZ;
double t1 = (b[0] - rayOrigin.x) * invX;
double t2 = (b[0] + 1 - rayOrigin.x) * invX;
double t3 = (b[1] - rayOrigin.y) * invY;
double t4 = (b[1] + 1 - rayOrigin.y) * invY;
double t5 = (b[2] - rayOrigin.z) * invZ;
double t6 = (b[2] + 1 - rayOrigin.z) * invZ;
double tmin = Math.max(Math.max(Math.min(t1, t2), Math.min(t3, t4)), Math.min(t5, t6));
double tmax = Math.min(Math.min(Math.max(t1, t2), Math.max(t3, t4)), Math.max(t5, t6));
// tmax > 0: 射线与AABB相交但AABB在身后
// tmin > tmax: 射线不相交
return tmax > 0 && tmin <= tmax;
}
/**
* 基于网格的光线追踪 (DDA算法)
*/
private boolean isVisible(MutableVec3d start, MutableVec3d[] targets, int size) {
int startX = cameraPos[0];
int startY = cameraPos[1];
int startZ = cameraPos[2];
for (int v = 0; v < size; v++) {
MutableVec3d target = targets[v];
double relX = start.x - target.x;
double relY = start.y - target.y;
double relZ = start.z - target.z;
// 优化避免在此处创建新的Vec3d对象进行归一化
if(allowRayChecks) {
double len = Math.sqrt(relX * relX + relY * relY + relZ * relZ);
// 传入归一化后的方向分量
if (rayIntersection(lastHitBlock, start, relX / len, relY / len, relZ / len)) {
continue;
}
}
double dimAbsX = Math.abs(relX);
double dimAbsY = Math.abs(relY);
double dimAbsZ = Math.abs(relZ);
double dimFracX = 1f / dimAbsX;
double dimFracY = 1f / dimAbsY;
double dimFracZ = 1f / dimAbsZ;
int intersectCount = 1;
int x_inc, y_inc, z_inc;
double t_next_y, t_next_x, t_next_z;
// 初始化DDA步进参数
if (dimAbsX == 0f) {
x_inc = 0; t_next_x = dimFracX;
} else if (target.x > start.x) {
x_inc = 1;
intersectCount += MiscUtils.fastFloor(target.x) - startX;
t_next_x = (startX + 1 - start.x) * dimFracX;
} else {
x_inc = -1;
intersectCount += startX - MiscUtils.fastFloor(target.x);
t_next_x = (start.x - startX) * dimFracX;
}
if (dimAbsY == 0f) {
y_inc = 0; t_next_y = dimFracY;
} else if (target.y > start.y) {
y_inc = 1;
intersectCount += MiscUtils.fastFloor(target.y) - startY;
t_next_y = (startY + 1 - start.y) * dimFracY;
} else {
y_inc = -1;
intersectCount += startY - MiscUtils.fastFloor(target.y);
t_next_y = (start.y - startY) * dimFracY;
}
if (dimAbsZ == 0f) {
z_inc = 0; t_next_z = dimFracZ;
} else if (target.z > start.z) {
z_inc = 1;
intersectCount += MiscUtils.fastFloor(target.z) - startZ;
t_next_z = (startZ + 1 - start.z) * dimFracZ;
} else {
z_inc = -1;
intersectCount += startZ - MiscUtils.fastFloor(target.z);
t_next_z = (start.z - startZ) * dimFracZ;
}
boolean finished = stepRay(startX, startY, startZ,
dimFracX, dimFracY, dimFracZ, intersectCount,
x_inc, y_inc, z_inc,
t_next_y, t_next_x, t_next_z);
provider.cleanup();
if (finished) {
cacheResult(targets[0], true);
return true;
} else {
allowRayChecks = true;
}
}
cacheResult(targets[0], false);
return false;
}
private boolean stepRay(int currentX, int currentY, int currentZ,
double distInX, double distInY, double distInZ,
int n, int x_inc, int y_inc, int z_inc,
double t_next_y, double t_next_x, double t_next_z) {
allowWallClipping = true; // 初始允许穿墙直到移出起始方块
for (; n > 1; n--) {
// 检查缓存状态2=遮挡
int cVal = getCacheValue(currentX, currentY, currentZ);
if (cVal == 2 && !allowWallClipping) {
lastHitBlock[0] = currentX; lastHitBlock[1] = currentY; lastHitBlock[2] = currentZ;
return false;
}
if (cVal == 0) {
// 未缓存查询Provider
int chunkX = currentX >> 4;
int chunkZ = currentZ >> 4;
if (!provider.prepareChunk(chunkX, chunkZ)) return false;
if (provider.isOpaqueFullCube(currentX, currentY, currentZ)) {
if (!allowWallClipping) {
cache.setLastHidden();
lastHitBlock[0] = currentX; lastHitBlock[1] = currentY; lastHitBlock[2] = currentZ;
return false;
}
} else {
allowWallClipping = false;
cache.setLastVisible();
}
} else if(cVal == 1) {
allowWallClipping = false;
}
// DDA算法选择下一个体素
if (t_next_y < t_next_x && t_next_y < t_next_z) {
currentY += y_inc;
t_next_y += distInY;
} else if (t_next_x < t_next_y && t_next_x < t_next_z) {
currentX += x_inc;
t_next_x += distInX;
} else {
currentZ += z_inc;
t_next_z += distInZ;
}
}
return true;
}
// 缓存状态:-1=无效, 0=未检查, 1=可见, 2=遮挡
private int getCacheValue(int x, int y, int z) {
x -= cameraPos[0];
y -= cameraPos[1];
z -= cameraPos[2];
if (Math.abs(x) > reach - 2 || Math.abs(y) > reach - 2 || Math.abs(z) > reach - 2) {
return -1;
}
return cache.getState(x + reach, y + reach, z + reach);
}
private void cacheResult(MutableVec3d vector, boolean result) {
int cx = MiscUtils.fastFloor(vector.x) - cameraPos[0] + reach;
int cy = MiscUtils.fastFloor(vector.y) - cameraPos[1] + reach;
int cz = MiscUtils.fastFloor(vector.z) - cameraPos[2] + reach;
if (result) cache.setVisible(cx, cy, cz);
else cache.setHidden(cx, cy, cz);
}
public void resetCache() {
this.cache.resetCache();
}
private enum Relative {
INSIDE, POSITIVE, NEGATIVE;
public static Relative from(int min, int max, int pos) {
if (max > pos && min > pos) return POSITIVE;
else if (min < pos && max < pos) return NEGATIVE;
return INSIDE;
}
}
public interface OcclusionCache {
void resetCache();
void setVisible(int x, int y, int z);
void setHidden(int x, int y, int z);
int getState(int x, int y, int z);
void setLastHidden();
void setLastVisible();
}
// 使用位运算压缩存储状态的缓存实现
public static class ArrayOcclusionCache implements OcclusionCache {
private final int reachX2;
private final byte[] cache;
private int entry, offset;
public ArrayOcclusionCache(int reach) {
this.reachX2 = reach * 2;
// 每一个位置占2位
this.cache = new byte[(reachX2 * reachX2 * reachX2) / 4 + 1];
}
@Override
public void resetCache() {
Arrays.fill(cache, (byte) 0);
}
private void calcIndex(int x, int y, int z) {
int positionKey = x + y * reachX2 + z * reachX2 * reachX2;
entry = positionKey / 4;
offset = (positionKey % 4) * 2;
}
@Override
public void setVisible(int x, int y, int z) {
calcIndex(x, y, z);
cache[entry] |= 1 << offset;
}
@Override
public void setHidden(int x, int y, int z) {
calcIndex(x, y, z);
cache[entry] |= 1 << (offset + 1);
}
@Override
public int getState(int x, int y, int z) {
calcIndex(x, y, z);
return (cache[entry] >> offset) & 3;
}
@Override
public void setLastVisible() {
cache[entry] |= 1 << offset;
}
@Override
public void setLastHidden() {
cache[entry] |= 1 << (offset + 1);
}
}
}

View File

@@ -35,4 +35,10 @@ public interface MessageConstants {
TranslatableComponent.Builder COMMAND_LOCALE_SET_FAILURE = Component.translatable().key("command.locale.set.failure");
TranslatableComponent.Builder COMMAND_LOCALE_SET_SUCCESS = Component.translatable().key("command.locale.set.success");
TranslatableComponent.Builder COMMAND_LOCALE_UNSET_SUCCESS = Component.translatable().key("command.locale.unset.success");
TranslatableComponent.Builder COMMAND_ITEM_CLEAR_SUCCESS_SINGLE = Component.translatable().key("command.item.clear.success.single");
TranslatableComponent.Builder COMMAND_ITEM_CLEAR_SUCCESS_MULTIPLE = Component.translatable().key("command.item.clear.success.multiple");
TranslatableComponent.Builder COMMAND_ITEM_CLEAR_FAILED_SINGLE = Component.translatable().key("command.item.clear.failed.single");
TranslatableComponent.Builder COMMAND_ITEM_CLEAR_FAILED_MULTIPLE = Component.translatable().key("command.item.clear.failed.multiple");
TranslatableComponent.Builder COMMAND_ITEM_CLEAR_TEST_SINGLE = Component.translatable().key("command.item.clear.test.single");
TranslatableComponent.Builder COMMAND_ITEM_CLEAR_TEST_MULTIPLE = Component.translatable().key("command.item.clear.test.multiple");
}

View File

@@ -37,7 +37,7 @@ public class TranslationManagerImpl implements TranslationManager {
private final Set<Locale> installed = ConcurrentHashMap.newKeySet();
private final Path translationsDirectory;
private final String langVersion;
private final String[] supportedLanguages;
private final Set<String> supportedLanguages;
private final Map<String, String> translationFallback = new LinkedHashMap<>();
private Locale selectedLocale = DEFAULT_LOCALE;
private MiniMessageTranslationRegistry registry;
@@ -52,7 +52,7 @@ public class TranslationManagerImpl implements TranslationManager {
this.plugin = plugin;
this.translationsDirectory = this.plugin.dataFolderPath().resolve("translations");
this.langVersion = PluginProperties.getValue("lang-version");
this.supportedLanguages = PluginProperties.getValue("supported-languages").split(",");
this.supportedLanguages = Arrays.stream(PluginProperties.getValue("supported-languages").split(",")).collect(Collectors.toSet());
this.langParser = new LangParser();
this.translationParser = new TranslationParser();
Yaml yaml = new Yaml(new TranslationConfigConstructor(new LoaderOptions()));
@@ -201,7 +201,7 @@ public class TranslationManagerImpl implements TranslationManager {
Map<String, String> data = yaml.load(inputStream);
if (data == null) return FileVisitResult.CONTINUE;
String langVersion = data.getOrDefault("lang-version", "");
if (!langVersion.equals(TranslationManagerImpl.this.langVersion)) {
if (!langVersion.equals(TranslationManagerImpl.this.langVersion) && TranslationManagerImpl.this.supportedLanguages.contains(localeName)) {
data = updateLangFile(data, path);
}
cachedFile = new CachedTranslation(data, lastModifiedTime, size);

View File

@@ -0,0 +1,4 @@
package net.momirealms.craftengine.core.util;
public class CustomDataType<T> {
}

View File

@@ -6,6 +6,8 @@ public interface LazyReference<T> {
T get();
boolean initialized();
static <T> LazyReference<T> lazyReference(final Supplier<T> supplier) {
return new LazyReference<>() {
private T value;
@@ -17,6 +19,11 @@ public interface LazyReference<T> {
}
return this.value;
}
@Override
public boolean initialized() {
return this.value != null;
}
};
}
}

View File

@@ -131,7 +131,7 @@ public class PngOptimizer {
private byte[] tryNormal(BufferedImage src, boolean hasAlpha, boolean isGrayscale) throws IOException {
byte[] bytes = generatePngData(src, hasAlpha, isGrayscale);
int zopfli = Config.zopfliIterations();
int zopfli = Config.optimizeTexture() ? Config.zopfliIterations() : 0;
return zopfli > 0 ? compressImageZopfli(bytes, zopfli) : compressImageStandard(bytes);
}
@@ -177,7 +177,7 @@ public class PngOptimizer {
writeChunkPLTE(paletteOs, palette);
}
byte[] bytes = generatePaletteData(src, palette);
int zopfli = Config.zopfliIterations();
int zopfli = Config.optimizeTexture() ? Config.zopfliIterations() : 0;
paletteOs.write(zopfli > 0 ? compressImageZopfli(bytes, zopfli) : compressImageStandard(bytes));
return Pair.of(palette, paletteOs.toByteArray());
}

View File

@@ -134,7 +134,7 @@ public final class ResourceConfigUtils {
}
case String s -> {
try {
return Integer.parseInt(s);
return Integer.parseInt(s.replace("_", ""));
} catch (NumberFormatException e) {
throw new LocalizedResourceConfigException("warning.config.type.int", e, s, option);
}
@@ -218,6 +218,30 @@ public final class ResourceConfigUtils {
}
}
public static long getAsLong(Object o, String option) {
switch (o) {
case null -> {
return 0;
}
case Long l -> {
return l;
}
case Number number -> {
return number.longValue();
}
case String s -> {
try {
return Long.parseLong(s.replace("_", ""));
} catch (NumberFormatException e) {
throw new LocalizedResourceConfigException("warning.config.type.long", e, s, option);
}
}
default -> {
throw new LocalizedResourceConfigException("warning.config.type.long", o.toString(), option);
}
}
}
@SuppressWarnings("unchecked")
public static Map<String, Object> getAsMap(Object obj, String option) {
if (obj instanceof Map<?, ?> map) {

View File

@@ -0,0 +1,127 @@
package net.momirealms.craftengine.core.world;
import net.momirealms.craftengine.core.util.MiscUtils;
public class MutableVec3d implements Position {
public double x;
public double y;
public double z;
public MutableVec3d(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}
public MutableVec3d toCenter() {
this.x = MiscUtils.fastFloor(x) + 0.5;
this.y = MiscUtils.fastFloor(y) + 0.5;
this.z = MiscUtils.fastFloor(z) + 0.5;
return this;
}
public MutableVec3d add(MutableVec3d vec) {
this.x += vec.x;
this.y += vec.y;
this.z += vec.z;
return this;
}
public MutableVec3d add(double x, double y, double z) {
this.x += x;
this.y += y;
this.z += z;
return this;
}
public MutableVec3d divide(MutableVec3d vec3d) {
this.x /= vec3d.x;
this.z /= vec3d.z;
this.y /= vec3d.y;
return this;
}
public MutableVec3d normalize() {
double mag = Math.sqrt(x * x + y * y + z * z);
this.x /= mag;
this.y /= mag;
this.z /= mag;
return this;
}
public static double distanceToSqr(MutableVec3d vec1, MutableVec3d vec2) {
double dx = vec2.x - vec1.x;
double dy = vec2.y - vec1.y;
double dz = vec2.z - vec1.z;
return dx * dx + dy * dy + dz * dz;
}
public void set(double x, double y, double z) {
this.x = x;
this.y = y;
this.z = z;
}
public void add(MutableVec3d vec3d, double x, double y, double z) {
this.x += (vec3d.x + x);
this.y += (vec3d.y + y);
this.z += (vec3d.z + z);
}
public void add(Vec3d vec3d, double x, double y, double z) {
this.x += (vec3d.x + x);
this.y += (vec3d.y + y);
this.z += (vec3d.z + z);
}
public void setX(double x) {
this.x = x;
}
public void setY(double y) {
this.y = y;
}
public void setZ(double z) {
this.z = z;
}
@Override
public double x() {
return x;
}
@Override
public double y() {
return y;
}
@Override
public double z() {
return z;
}
@Override
public final boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof MutableVec3d vec3d)) return false;
return this.x == vec3d.x && this.y == vec3d.y && this.z == vec3d.z;
}
@Override
public int hashCode() {
int result = Double.hashCode(x);
result = 31 * result + Double.hashCode(y);
result = 31 * result + Double.hashCode(z);
return result;
}
@Override
public String toString() {
return "Vec3d{" +
"x=" + x +
", y=" + y +
", z=" + z +
'}';
}
}

View File

@@ -72,7 +72,7 @@ public class Vec3d implements Position {
public final boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Vec3d vec3d)) return false;
return Double.compare(x, vec3d.x) == 0 && Double.compare(y, vec3d.y) == 0 && Double.compare(z, vec3d.z) == 0;
return this.x == vec3d.x && this.y == vec3d.y && this.z == vec3d.z;
}
@Override