mirror of
https://github.com/Xiao-MoMi/craft-engine.git
synced 2026-01-06 15:52:03 +00:00
@@ -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")
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"))
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>")),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package net.momirealms.craftengine.core.util;
|
||||
|
||||
public class CustomDataType<T> {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user