9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2025-12-30 12:29:15 +00:00

Merge pull request #402 from Xiao-MoMi/main

更新上游
This commit is contained in:
XiaoMoMi
2025-10-07 00:10:19 +08:00
committed by GitHub
343 changed files with 16995 additions and 12105 deletions

View File

@@ -1,5 +1,5 @@
plugins {
id("com.gradleup.shadow") version "9.0.0-rc2"
id("com.gradleup.shadow") version "9.2.2"
id("maven-publish")
}
@@ -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.5")
implementation("net.momirealms:craft-engine-s3:0.6")
// Util
compileOnly("net.momirealms:sparrow-util:${rootProject.properties["sparrow_util_version"]}")
// Adventure
@@ -30,6 +30,8 @@ dependencies {
compileOnly("net.kyori:adventure-text-serializer-gson:${rootProject.properties["adventure_bundle_version"]}") {
exclude("com.google.code.gson", "gson")
}
compileOnly("net.kyori:adventure-text-serializer-legacy:${rootProject.properties["adventure_bundle_version"]}")
compileOnly("net.kyori:adventure-text-serializer-json-legacy-impl:${rootProject.properties["adventure_bundle_version"]}")
// Command
compileOnly("org.incendo:cloud-core:${rootProject.properties["cloud_core_version"]}")

View File

@@ -1,69 +1,120 @@
package net.momirealms.craftengine.core.block;
import com.google.common.collect.ImmutableList;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import net.momirealms.craftengine.core.block.behavior.EntityBlockBehavior;
import net.momirealms.craftengine.core.block.entity.render.element.BlockEntityElement;
import net.momirealms.craftengine.core.block.entity.render.element.BlockEntityElementConfig;
import net.momirealms.craftengine.core.block.entity.render.element.BlockEntityElementConfigs;
import net.momirealms.craftengine.core.block.parser.BlockNbtParser;
import net.momirealms.craftengine.core.block.properties.Properties;
import net.momirealms.craftengine.core.block.properties.Property;
import net.momirealms.craftengine.core.loot.LootTable;
import net.momirealms.craftengine.core.pack.LoadingSequence;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.pack.PendingConfigSection;
import net.momirealms.craftengine.core.pack.ResourceLocation;
import net.momirealms.craftengine.core.pack.allocator.BlockStateCandidate;
import net.momirealms.craftengine.core.pack.allocator.IdAllocator;
import net.momirealms.craftengine.core.pack.allocator.VisualBlockStateAllocator;
import net.momirealms.craftengine.core.pack.model.generation.AbstractModelGenerator;
import net.momirealms.craftengine.core.pack.model.generation.ModelGeneration;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.config.ConfigParser;
import net.momirealms.craftengine.core.plugin.config.IdSectionConfigParser;
import net.momirealms.craftengine.core.plugin.config.SectionConfigParser;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.context.event.EventFunctions;
import net.momirealms.craftengine.core.plugin.context.event.EventTrigger;
import net.momirealms.craftengine.core.plugin.context.function.Function;
import net.momirealms.craftengine.core.plugin.locale.LocalizedException;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.util.GsonHelper;
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.plugin.logger.Debugger;
import net.momirealms.craftengine.core.registry.BuiltInRegistries;
import net.momirealms.craftengine.core.registry.Holder;
import net.momirealms.craftengine.core.registry.WritableRegistry;
import net.momirealms.craftengine.core.util.*;
import net.momirealms.sparrow.nbt.CompoundTag;
import org.incendo.cloud.suggestion.Suggestion;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Function;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
public abstract class AbstractBlockManager extends AbstractModelGenerator implements BlockManager {
protected final BlockParser blockParser;
// CraftEngine objects
protected final BlockStateMappingParser blockStateMappingParser;
// 根据id获取自定义方块
protected final Map<Key, CustomBlock> byId = new HashMap<>();
// Cached command suggestions
// 缓存的指令建议
protected final List<Suggestion> cachedSuggestions = new ArrayList<>();
// Cached Namespace
// 缓存的使用中的命名空间
protected final Set<String> namespacesInUse = new HashSet<>();
// for mod, real block id -> state models
protected final Map<Key, JsonElement> modBlockStates = new HashMap<>();
// A temporary map that stores the model path of a certain vanilla block state
protected final Map<Integer, JsonElement> tempVanillaBlockStateModels = new Int2ObjectOpenHashMap<>();
// A temporary map used to detect whether the same block state corresponds to multiple models.
protected final Map<Integer, Key> tempRegistryIdConflictMap = new Int2ObjectOpenHashMap<>();
// A temporary map that converts the custom block registered on the server to the vanilla block ID.
protected final Map<Integer, Integer> tempBlockAppearanceConvertor = new Int2IntOpenHashMap();
// Used to store override information of json files
// Map<方块类型, Map<方块状态NBT,模型>>用于生成block state json
protected final Map<Key, Map<String, JsonElement>> blockStateOverrides = new HashMap<>();
// a reverted mapper
// 用于生成mod使用的block state json
protected final Map<Key, JsonElement> modBlockStateOverrides = new HashMap<>();
// 根据外观查找真实状态用于debug指令
protected final Map<Integer, List<Integer>> appearanceToRealState = new Int2ObjectOpenHashMap<>();
// 用于note_block:0这样格式的自动分配
protected final Map<Key, List<BlockStateWrapper>> blockStateArranger = new HashMap<>();
// 全方块状态映射文件,用于网络包映射
protected final int[] blockStateMappings;
// 原版方块状态数量
protected final int vanillaBlockStateCount;
// 注册的大宝贝
protected final DelegatingBlock[] customBlocks;
protected final DelegatingBlockState[] customBlockStates;
protected final Object[] customBlockHolders;
// 自定义状态列表,会随着重载变化
protected final ImmutableBlockState[] immutableBlockStates;
// 倒推缓存
protected final BlockStateCandidate[] autoVisualBlockStateCandidates;
// 用于检测单个外观方块状态是否被绑定了不同模型
protected final JsonElement[] tempVanillaBlockStateModels;
// 临时存储哪些视觉方块被使用了
protected final Set<BlockStateWrapper> tempVisualBlockStatesInUse = new HashSet<>();
protected final Set<Key> tempVisualBlocksInUse = new HashSet<>();
// 声音映射表,和使用了哪些视觉方块有关
protected Map<Key, Key> soundReplacements = Map.of();
// client side block tags
protected Map<Integer, List<String>> clientBoundTags = Map.of();
protected Map<Integer, List<String>> previousClientBoundTags = Map.of();
// Used to automatically arrange block states for strings such as minecraft:note_block:0
protected Map<Key, List<Integer>> blockAppearanceArranger;
protected Map<Key, List<Integer>> realBlockArranger;
protected Map<Key, Integer> internalId2StateId;
protected Map<Key, DelegatingBlock> registeredBlocks;
protected AbstractBlockManager(CraftEngine plugin) {
protected AbstractBlockManager(CraftEngine plugin, int vanillaBlockStateCount, int customBlockCount) {
super(plugin);
this.blockParser = new BlockParser();
this.vanillaBlockStateCount = vanillaBlockStateCount;
this.customBlocks = new DelegatingBlock[customBlockCount];
this.customBlockHolders = new Object[customBlockCount];
this.customBlockStates = new DelegatingBlockState[customBlockCount];
this.immutableBlockStates = new ImmutableBlockState[customBlockCount];
this.blockStateMappings = new int[customBlockCount + vanillaBlockStateCount];
this.autoVisualBlockStateCandidates = new BlockStateCandidate[vanillaBlockStateCount];
this.tempVanillaBlockStateModels = new JsonElement[vanillaBlockStateCount];
this.blockParser = new BlockParser(this.autoVisualBlockStateCandidates);
this.blockStateMappingParser = new BlockStateMappingParser();
Arrays.fill(this.blockStateMappings, -1);
}
@NotNull
@Override
public ImmutableBlockState getImmutableBlockStateUnsafe(int stateId) {
return this.immutableBlockStates[stateId - this.vanillaBlockStateCount];
}
@Nullable
@Override
public ImmutableBlockState getImmutableBlockState(int stateId) {
if (!isVanillaBlockState(stateId)) {
return this.immutableBlockStates[stateId - this.vanillaBlockStateCount];
}
return null;
}
@Override
@@ -71,19 +122,26 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
super.clearModelsToGenerate();
this.clearCache();
this.cachedSuggestions.clear();
this.namespacesInUse.clear();
this.blockStateOverrides.clear();
this.modBlockStates.clear();
this.modBlockStateOverrides.clear();
this.byId.clear();
this.previousClientBoundTags = this.clientBoundTags;
this.clientBoundTags = new HashMap<>();
this.blockStateArranger.clear();
this.appearanceToRealState.clear();
Arrays.fill(this.blockStateMappings, -1);
Arrays.fill(this.immutableBlockStates, EmptyBlock.STATE);
Arrays.fill(this.autoVisualBlockStateCandidates, null);
for (AutoStateGroup autoStateGroup : AutoStateGroup.values()) {
autoStateGroup.reset();
}
}
@Override
public void delayedLoad() {
this.initSuggestions();
this.clearCache();
this.resendTags();
this.processSounds();
this.clearCache();
}
@Override
@@ -96,24 +154,20 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
return Optional.ofNullable(this.byId.get(id));
}
protected void addBlockInternal(Key id, CustomBlock customBlock) {
this.byId.put(id, customBlock);
// generate mod assets
if (Config.generateModAssets()) {
for (ImmutableBlockState state : customBlock.variantProvider().states()) {
this.modBlockStates.put(getBlockOwnerId(state.customBlockState()), this.tempVanillaBlockStateModels.get(state.vanillaBlockState().registryId()));
}
}
public Map<Key, List<BlockStateWrapper>> blockStateArranger() {
return this.blockStateArranger;
}
protected abstract void applyPlatformSettings(ImmutableBlockState state);
@Override
public ConfigParser parser() {
return this.blockParser;
public ConfigParser[] parsers() {
return new ConfigParser[]{this.blockParser, this.blockStateMappingParser};
}
@Override
public Map<Key, JsonElement> modBlockStates() {
return Collections.unmodifiableMap(this.modBlockStates);
return Collections.unmodifiableMap(this.modBlockStateOverrides);
}
@Override
@@ -126,14 +180,24 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
return Collections.unmodifiableCollection(this.cachedSuggestions);
}
@Nullable
public Key replaceSoundIfExist(Key id) {
return this.soundReplacements.get(id);
}
@Override
public Map<Key, Key> soundReplacements() {
return Collections.unmodifiableMap(this.soundReplacements);
}
public Set<String> namespacesInUse() {
return Collections.unmodifiableSet(this.namespacesInUse);
}
protected void clearCache() {
this.tempRegistryIdConflictMap.clear();
this.tempBlockAppearanceConvertor.clear();
this.tempVanillaBlockStateModels.clear();
Arrays.fill(this.tempVanillaBlockStateModels, null);
this.tempVisualBlockStatesInUse.clear();
this.tempVisualBlocksInUse.clear();
}
protected void initSuggestions() {
@@ -157,20 +221,149 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
return Optional.ofNullable(this.appearanceToRealState.get(appearanceStateId)).orElse(List.of());
}
public abstract BlockBehavior createBlockBehavior(CustomBlock customBlock, List<Map<String, Object>> behaviorConfig);
protected abstract void resendTags();
protected abstract boolean isVanillaBlock(Key id);
protected abstract int getBlockRegistryId(Key id);
protected abstract String stateRegistryIdToStateSNBT(int id);
protected abstract Key getBlockOwnerId(int id);
protected abstract CustomBlock.Builder platformBuilder(Key id);
protected abstract void setVanillaBlockTags(Key id, List<String> tags);
public class BlockParser implements ConfigParser {
protected abstract int vanillaBlockStateCount();
protected abstract void processSounds();
protected abstract CustomBlock createCustomBlock(@NotNull Holder.Reference<CustomBlock> holder,
@NotNull BlockStateVariantProvider variantProvider,
@NotNull Map<EventTrigger, List<Function<PlayerOptionalContext>>> events,
@Nullable LootTable<?> lootTable);
public class BlockStateMappingParser extends SectionConfigParser {
public static final String[] CONFIG_SECTION_NAME = new String[]{"block-state-mappings", "block-state-mapping"};
@Override
public String[] sectionId() {
return CONFIG_SECTION_NAME;
}
@Override
public int loadingSequence() {
return LoadingSequence.BLOCK_STATE_MAPPING;
}
@Override
public void parseSection(Pack pack, Path path, Map<String, Object> section) throws LocalizedException {
ExceptionCollector<LocalizedResourceConfigException> exceptionCollector = new ExceptionCollector<>();
for (Map.Entry<String, Object> entry : section.entrySet()) {
String before = entry.getKey();
String after = entry.getValue().toString();
// 先解析为唯一的wrapper
BlockStateWrapper beforeState = createVanillaBlockState(before);
BlockStateWrapper afterState = createVanillaBlockState(after);
if (beforeState == null) {
exceptionCollector.add(new LocalizedResourceConfigException("warning.config.block_state_mapping.invalid_state", before));
continue;
}
if (afterState == null) {
exceptionCollector.add(new LocalizedResourceConfigException("warning.config.block_state_mapping.invalid_state", after));
continue;
}
int previous = AbstractBlockManager.this.blockStateMappings[beforeState.registryId()];
if (previous != -1 && previous != afterState.registryId()) {
exceptionCollector.add(new LocalizedResourceConfigException("warning.config.block_state_mapping.conflict",
beforeState.toString(),
afterState.toString(),
BlockRegistryMirror.byId(previous).toString()));
continue;
}
AbstractBlockManager.this.blockStateMappings[beforeState.registryId()] = afterState.registryId();
Key blockOwnerId = getBlockOwnerId(beforeState);
List<BlockStateWrapper> blockStateWrappers = AbstractBlockManager.this.blockStateArranger.computeIfAbsent(blockOwnerId, k -> new ArrayList<>());
blockStateWrappers.add(beforeState);
AbstractBlockManager.this.autoVisualBlockStateCandidates[beforeState.registryId()] = createVisualBlockCandidate(beforeState);
}
exceptionCollector.throwIfPresent();
}
@Nullable
public BlockStateCandidate createVisualBlockCandidate(BlockStateWrapper blockState) {
List<AutoStateGroup> groups = AutoStateGroup.findGroups(blockState);
if (!groups.isEmpty()) {
BlockStateCandidate candidate = new BlockStateCandidate(blockState);
for (AutoStateGroup group : groups) {
group.addCandidate(candidate);
}
return candidate;
}
return null;
}
}
public class BlockParser extends IdSectionConfigParser {
public static final String[] CONFIG_SECTION_NAME = new String[]{"blocks", "block"};
private final IdAllocator internalIdAllocator;
private final VisualBlockStateAllocator visualBlockStateAllocator;
private final List<PendingConfigSection> pendingConfigSections = new ArrayList<>();
public BlockParser(BlockStateCandidate[] candidates) {
this.internalIdAllocator = new IdAllocator(AbstractBlockManager.this.plugin.dataFolderPath().resolve("cache").resolve("custom-block-states.json"));
this.visualBlockStateAllocator = new VisualBlockStateAllocator(AbstractBlockManager.this.plugin.dataFolderPath().resolve("cache").resolve("visual-block-states.json"), candidates, AbstractBlockManager.this::createVanillaBlockState);
}
public void addPendingConfigSection(PendingConfigSection section) {
this.pendingConfigSections.add(section);
}
public IdAllocator internalIdAllocator() {
return internalIdAllocator;
}
public VisualBlockStateAllocator visualBlockStateAllocator() {
return visualBlockStateAllocator;
}
@Override
public void postProcess() {
this.internalIdAllocator.processPendingAllocations();
try {
this.internalIdAllocator.saveToCache();
} catch (IOException e) {
AbstractBlockManager.this.plugin.logger().warn("Error while saving custom block states allocation", e);
}
this.visualBlockStateAllocator.processPendingAllocations();
try {
this.visualBlockStateAllocator.saveToCache();
} catch (IOException e) {
AbstractBlockManager.this.plugin.logger().warn("Error while saving visual block states allocation", e);
}
}
@Override
public void preProcess() {
this.internalIdAllocator.reset(0, Config.serverSideBlocks() - 1);
this.visualBlockStateAllocator.reset();
try {
this.visualBlockStateAllocator.loadFromCache();
} catch (IOException e) {
AbstractBlockManager.this.plugin.logger().warn("Error while loading visual block states allocation cache", e);
}
try {
this.internalIdAllocator.loadFromCache();
} catch (IOException e) {
AbstractBlockManager.this.plugin.logger().warn("Error while loading custom block states allocation cache", e);
}
for (PendingConfigSection section : this.pendingConfigSections) {
ResourceConfigUtils.runCatching(
section.path(),
section.node(),
() -> parseSection(section.pack(), section.path(), section.node(), section.id(), section.config()),
() -> GsonHelper.get().toJson(section.config())
);
}
this.pendingConfigSections.clear();
}
@Override
public String[] sectionId() {
@@ -183,119 +376,278 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
}
@Override
public void parseSection(Pack pack, Path path, Key id, Map<String, Object> section) {
public void parseSection(Pack pack, Path path, String node, Key id, Map<String, Object> section) {
if (isVanillaBlock(id)) {
parseVanillaBlock(pack, path, id, section);
parseVanillaBlock(id, section);
} else {
// check duplicated config
if (AbstractBlockManager.this.byId.containsKey(id)) {
throw new LocalizedResourceConfigException("warning.config.block.duplicate");
}
parseCustomBlock(pack, path, id, section);
parseCustomBlock(path, node, id, section);
}
}
private void parseVanillaBlock(Pack pack, Path path, Key id, Map<String, Object> section) {
private void parseVanillaBlock(Key id, Map<String, Object> section) {
Map<String, Object> settings = MiscUtils.castToMap(section.get("settings"), true);
if (settings != null) {
Object clientBoundTags = settings.get("client-bound-tags");
if (clientBoundTags instanceof List<?> list) {
List<String> clientSideTags = MiscUtils.getAsStringList(list).stream().filter(ResourceLocation::isValid).toList();
AbstractBlockManager.this.clientBoundTags.put(getBlockRegistryId(id), clientSideTags);
AbstractBlockManager.this.setVanillaBlockTags(id, clientSideTags);
}
}
}
private void parseCustomBlock(Pack pack, Path path, Key id, Map<String, Object> section) {
// 获取方块设置
private void parseCustomBlock(Path path, String node, Key id, Map<String, Object> section) {
// 获取共享方块设置
BlockSettings settings = BlockSettings.fromMap(id, MiscUtils.castToMap(section.get("settings"), true));
// 读取基础外观配置
Map<String, Property<?>> properties;
Map<String, BlockStateAppearance> appearances;
Map<String, BlockStateVariant> variants;
// 读取states区域
Map<String, Object> stateSection = MiscUtils.castToMap(ResourceConfigUtils.requireNonNullOrThrow(
ResourceConfigUtils.get(section, "state", "states"), "warning.config.block.missing_state"), true);
Map<String, Object> stateSection = MiscUtils.castToMap(ResourceConfigUtils.requireNonNullOrThrow(ResourceConfigUtils.get(section, "state", "states"), "warning.config.block.missing_state"), true);
boolean singleState = !stateSection.containsKey("properties");
// 单方块状态
if (singleState) {
int internalId = ResourceConfigUtils.getAsInt(ResourceConfigUtils.requireNonNullOrThrow(
stateSection.get("id"), "warning.config.block.state.missing_real_id"), "id");
// 获取原版外观的注册表id
int appearanceId = pluginFormattedBlockStateToRegistryId(ResourceConfigUtils.requireNonEmptyStringOrThrow(
stateSection.get("state"), "warning.config.block.state.missing_state"));
Optional<BlockEntityElementConfig<? extends BlockEntityElement>[]> blockEntityRenderer = parseBlockEntityRender(stateSection.get("entity-renderer"));
// 读取方块的property通过property决定
Map<String, Property<?>> properties = singleState ? Map.of() : parseBlockProperties(ResourceConfigUtils.getAsMap(ResourceConfigUtils.requireNonNullOrThrow(stateSection.get("properties"), "warning.config.block.state.missing_properties"), "properties"));
// 注册方块容器
Holder.Reference<CustomBlock> holder = ((WritableRegistry<CustomBlock>) BuiltInRegistries.BLOCK).getOrRegisterForHolder(ResourceKey.create(BuiltInRegistries.BLOCK.key().location(), id));
// 先绑定无效方块
holder.bindValue(new InactiveCustomBlock(holder));
// 为原版外观赋予外观模型并检查模型冲突
this.arrangeModelForStateAndVerify(appearanceId, ResourceConfigUtils.get(stateSection, "model", "models"));
// 设置参数
properties = Map.of();
appearances = Map.of("", new BlockStateAppearance(appearanceId, blockEntityRenderer));
variants = Map.of("", new BlockStateVariant("", settings, getInternalBlockId(internalId, appearanceId)));
}
// 多方块状态
else {
properties = parseBlockProperties(ResourceConfigUtils.getAsMap(ResourceConfigUtils.requireNonNullOrThrow(stateSection.get("properties"), "warning.config.block.state.missing_properties"), "properties"));
appearances = parseBlockAppearances(ResourceConfigUtils.getAsMap(ResourceConfigUtils.requireNonNullOrThrow(stateSection.get("appearances"), "warning.config.block.state.missing_appearances"), "appearances"));
variants = parseBlockVariants(
ResourceConfigUtils.getAsMap(ResourceConfigUtils.requireNonNullOrThrow(stateSection.get("variants"), "warning.config.block.state.missing_variants"), "variants"),
it -> {
BlockStateAppearance blockStateAppearance = appearances.get(it);
return blockStateAppearance == null ? -1 : blockStateAppearance.stateRegistryId();
}, settings
);
}
// 根据properties生成variant provider
BlockStateVariantProvider variantProvider = new BlockStateVariantProvider(holder, (owner, propertyMap) -> {
ImmutableBlockState blockState = new ImmutableBlockState(owner, propertyMap);
blockState.setSettings(settings);
return blockState;
}, properties);
addBlockInternal(id, platformBuilder(id)
.appearances(appearances)
.variantMapper(variants)
.properties(properties)
.settings(settings)
.lootTable(LootTable.fromMap(ResourceConfigUtils.getAsMapOrNull(section.get("loot"), "loot")))
.behavior(MiscUtils.getAsMapList(ResourceConfigUtils.get(section, "behavior", "behaviors")))
.events(EventFunctions.parseEvents(ResourceConfigUtils.get(section, "events", "event")))
.build());
}
ImmutableList<ImmutableBlockState> states = variantProvider.states();
List<CompletableFuture<Integer>> internalIdAllocators = new ArrayList<>(states.size());
private Map<String, BlockStateVariant> parseBlockVariants(Map<String, Object> variantsSection,
Function<String, Integer> appearanceValidator,
BlockSettings parentSettings) {
Map<String, BlockStateVariant> variants = new HashMap<>();
for (Map.Entry<String, Object> entry : variantsSection.entrySet()) {
Map<String, Object> variantSection = ResourceConfigUtils.getAsMap(entry.getValue(), entry.getKey());
String variantNBT = entry.getKey();
String appearance = ResourceConfigUtils.requireNonEmptyStringOrThrow(variantSection.get("appearance"), "warning.config.block.state.variant.missing_appearance");
int appearanceId = appearanceValidator.apply(appearance);
if (appearanceId == -1) {
throw new LocalizedResourceConfigException("warning.config.block.state.variant.invalid_appearance", variantNBT, appearance);
// 如果用户指定了起始id
if (stateSection.containsKey("id")) {
int startingId = ResourceConfigUtils.getAsInt(stateSection.get("id"), "id");
int endingId = startingId + states.size() - 1;
if (startingId < 0 || endingId >= Config.serverSideBlocks()) {
throw new LocalizedResourceConfigException("warning.config.block.state.invalid_id", startingId + "~" + endingId, String.valueOf(Config.serverSideBlocks() - 1));
}
// 先检测范围冲突
List<Pair<String, Integer>> conflicts = this.internalIdAllocator.getFixedIdsBetween(startingId, endingId);
if (!conflicts.isEmpty()) {
ExceptionCollector<LocalizedResourceConfigException> exceptionCollector = new ExceptionCollector<>();
for (Pair<String, Integer> conflict : conflicts) {
int internalId = conflict.right();
int index = internalId - startingId;
exceptionCollector.add(new LocalizedResourceConfigException("warning.config.block.state.id.conflict", states.get(index).toString(), conflict.left(), BlockManager.createCustomBlockKey(internalId).toString()));
}
exceptionCollector.throwIfPresent();
}
// 强行分配id
for (ImmutableBlockState blockState : states) {
String blockStateId = blockState.toString();
internalIdAllocators.add(this.internalIdAllocator.assignFixedId(blockStateId, startingId++));
}
}
// 未指定,则使用自动分配
else {
for (ImmutableBlockState blockState : states) {
String blockStateId = blockState.toString();
internalIdAllocators.add(this.internalIdAllocator.requestAutoId(blockStateId));
}
int internalId = getInternalBlockId(ResourceConfigUtils.getAsInt(ResourceConfigUtils.requireNonNullOrThrow(variantSection.get("id"), "warning.config.block.state.missing_real_id"), "id"), appearanceId);
Map<String, Object> anotherSetting = ResourceConfigUtils.getAsMapOrNull(variantSection.get("settings"), "settings");
variants.put(variantNBT, new BlockStateVariant(appearance, anotherSetting == null ? parentSettings : BlockSettings.ofFullCopy(parentSettings, anotherSetting), internalId));
}
return variants;
}
private int getInternalBlockId(int internalId, int appearanceId) {
Key baseBlock = getBlockOwnerId(appearanceId);
Key internalBlockId = Key.of(Key.DEFAULT_NAMESPACE, baseBlock.value() + "_" + internalId);
int internalBlockRegistryId = Optional.ofNullable(AbstractBlockManager.this.internalId2StateId.get(internalBlockId)).orElse(-1);
if (internalBlockRegistryId == -1) {
throw new LocalizedResourceConfigException("warning.config.block.state.invalid_real_id", internalBlockId.toString(), String.valueOf(availableAppearances(baseBlock) - 1));
}
return internalBlockRegistryId;
}
CompletableFutures.allOf(internalIdAllocators).whenComplete((v1, t1) -> ResourceConfigUtils.runCatching(path, node, () -> {
if (t1 != null) {
if (t1 instanceof CompletionException e) {
Throwable cause = e.getCause();
// 这里不会有conflict了因为之前已经判断过了
if (cause instanceof IdAllocator.IdExhaustedException) {
throw new LocalizedResourceConfigException("warning.config.block.state.id.exhausted");
} else {
Debugger.BLOCK.warn(() -> "Unknown error while allocating internal block state id.", cause);
return;
}
}
throw new RuntimeException("Unknown error occurred", t1);
}
private Map<String, BlockStateAppearance> parseBlockAppearances(Map<String, Object> appearancesSection) {
Map<String, BlockStateAppearance> appearances = new HashMap<>();
for (Map.Entry<String, Object> entry : appearancesSection.entrySet()) {
Map<String, Object> appearanceSection = ResourceConfigUtils.getAsMap(entry.getValue(), entry.getKey());
int appearanceId = pluginFormattedBlockStateToRegistryId(ResourceConfigUtils.requireNonEmptyStringOrThrow(
appearanceSection.get("state"), "warning.config.block.state.missing_state"));
this.arrangeModelForStateAndVerify(appearanceId, ResourceConfigUtils.get(appearanceSection, "model", "models"));
appearances.put(entry.getKey(), new BlockStateAppearance(appearanceId, parseBlockEntityRender(appearanceSection.get("entity-renderer"))));
}
return appearances;
for (int i = 0; i < internalIdAllocators.size(); i++) {
CompletableFuture<Integer> future = internalIdAllocators.get(i);
try {
int internalId = future.get();
states.get(i).setCustomBlockState(BlockRegistryMirror.byId(internalId + AbstractBlockManager.this.vanillaBlockStateCount));
} catch (InterruptedException | ExecutionException e) {
AbstractBlockManager.this.plugin.logger().warn("Interrupted while allocating internal block state for block " + id.asString(), e);
return;
}
}
// 创建自定义方块
AbstractCustomBlock customBlock = (AbstractCustomBlock) createCustomBlock(
holder,
variantProvider,
EventFunctions.parseEvents(ResourceConfigUtils.get(section, "events", "event")),
LootTable.fromMap(ResourceConfigUtils.getAsMapOrNull(section.get("loot"), "loot"))
);
BlockBehavior blockBehavior = createBlockBehavior(customBlock, MiscUtils.getAsMapList(ResourceConfigUtils.get(section, "behavior", "behaviors")));
Map<String, Map<String, Object>> appearanceConfigs;
Map<String, CompletableFuture<BlockStateWrapper>> futureVisualStates = new HashMap<>();
if (singleState) {
appearanceConfigs = Map.of("", stateSection);
} else {
Map<String, Object> rawAppearancesSection = ResourceConfigUtils.getAsMap(ResourceConfigUtils.requireNonNullOrThrow(stateSection.get("appearances"), "warning.config.block.state.missing_appearances"), "appearances");
appearanceConfigs = new LinkedHashMap<>(4);
for (Map.Entry<String, Object> entry : rawAppearancesSection.entrySet()) {
appearanceConfigs.put(entry.getKey(), ResourceConfigUtils.getAsMap(entry.getValue(), entry.getKey()));
}
}
for (Map.Entry<String, Map<String, Object>> entry : appearanceConfigs.entrySet()) {
Map<String, Object> appearanceSection = entry.getValue();
if (appearanceSection.containsKey("state")) {
String appearanceName = entry.getKey();
futureVisualStates.put(
appearanceName,
this.visualBlockStateAllocator.assignFixedBlockState(appearanceName.isEmpty() ? id.asString() : id.asString() + ":" + appearanceName, parsePluginFormattedBlockState(appearanceSection.get("state").toString()))
);
} else if (appearanceSection.containsKey("auto-state")) {
String autoStateId = appearanceSection.get("auto-state").toString();
AutoStateGroup group = AutoStateGroup.byId(autoStateId);
if (group == null) {
throw new LocalizedResourceConfigException("warning.config.block.state.invalid_auto_state", autoStateId, EnumUtils.toString(AutoStateGroup.values()));
}
String appearanceName = entry.getKey();
futureVisualStates.put(
appearanceName,
this.visualBlockStateAllocator.requestAutoState(appearanceName.isEmpty() ? id.asString() : id.asString() + ":" + appearanceName, group)
);
} else {
throw new LocalizedResourceConfigException("warning.config.block.state.missing_state");
}
}
CompletableFutures.allOf(futureVisualStates.values()).whenComplete((v2, t2) -> ResourceConfigUtils.runCatching(path, node, () -> {
if (t2 != null) {
if (t2 instanceof CompletionException e) {
Throwable cause = e.getCause();
if (cause instanceof VisualBlockStateAllocator.StateExhaustedException exhausted) {
throw new LocalizedResourceConfigException("warning.config.block.state.auto_state.exhausted", exhausted.group().id(), String.valueOf(exhausted.group().candidateCount()));
} else {
Debugger.BLOCK.warn(() -> "Unknown error while allocating visual block state.", cause);
return;
}
}
throw new RuntimeException("Unknown error occurred", t2);
}
BlockStateAppearance anyAppearance = null;
Map<String, BlockStateAppearance> appearances = new HashMap<>();
for (Map.Entry<String, Map<String, Object>> entry : appearanceConfigs.entrySet()) {
String appearanceName = entry.getKey();
Map<String, Object> appearanceSection = entry.getValue();
BlockStateWrapper visualBlockState;
try {
visualBlockState = futureVisualStates.get(appearanceName).get();
} catch (InterruptedException | ExecutionException e) {
AbstractBlockManager.this.plugin.logger().warn("Interrupted while allocating visual block state for block " + id.asString(), e);
return;
}
this.arrangeModelForStateAndVerify(visualBlockState, ResourceConfigUtils.get(appearanceSection, "model", "models"));
BlockStateAppearance blockStateAppearance = new BlockStateAppearance(visualBlockState, parseBlockEntityRender(appearanceSection.get("entity-renderer")));
appearances.put(appearanceName, blockStateAppearance);
if (anyAppearance == null) {
anyAppearance = blockStateAppearance;
}
}
// 至少有一个外观吧
Objects.requireNonNull(anyAppearance, "any appearance should not be null");
ExceptionCollector<LocalizedResourceConfigException> exceptionCollector = new ExceptionCollector<>();
if (!singleState) {
Map<String, Object> variantsSection = ResourceConfigUtils.getAsMapOrNull(stateSection.get("variants"), "variants");
if (variantsSection != null) {
for (Map.Entry<String, Object> entry : variantsSection.entrySet()) {
Map<String, Object> variantSection = ResourceConfigUtils.getAsMap(entry.getValue(), entry.getKey());
String variantNBT = entry.getKey();
// 先解析nbt找到需要修改的方块状态
CompoundTag tag = BlockNbtParser.deserialize(variantProvider, variantNBT);
if (tag == null) {
exceptionCollector.add(new LocalizedResourceConfigException("warning.config.block.state.property.invalid_format", variantNBT));
continue;
}
List<ImmutableBlockState> possibleStates = variantProvider.getPossibleStates(tag);
Map<String, Object> anotherSetting = ResourceConfigUtils.getAsMapOrNull(variantSection.get("settings"), "settings");
if (anotherSetting != null) {
for (ImmutableBlockState possibleState : possibleStates) {
possibleState.setSettings(BlockSettings.ofFullCopy(possibleState.settings(), anotherSetting));
}
}
String appearanceName = ResourceConfigUtils.getAsString(variantSection.get("appearance"));
if (appearanceName != null) {
BlockStateAppearance appearance = appearances.get(appearanceName);
if (appearance == null) {
exceptionCollector.add(new LocalizedResourceConfigException("warning.config.block.state.variant.invalid_appearance", variantNBT, appearanceName));
continue;
}
for (ImmutableBlockState possibleState : possibleStates) {
possibleState.setVanillaBlockState(appearance.blockState());
appearance.blockEntityRenderer().ifPresent(possibleState::setConstantRenderers);
}
}
}
}
}
// 获取方块实体行为
EntityBlockBehavior entityBlockBehavior = blockBehavior.getEntityBehavior();
boolean isEntityBlock = entityBlockBehavior != null;
// 绑定行为
for (ImmutableBlockState state : states) {
if (isEntityBlock) {
state.setBlockEntityType(entityBlockBehavior.blockEntityType());
}
state.setBehavior(blockBehavior);
int internalId = state.customBlockState().registryId();
BlockStateWrapper visualState = state.vanillaBlockState();
// 校验,为未绑定外观的强行添加外观
if (visualState == null) {
visualState = anyAppearance.blockState();
state.setVanillaBlockState(visualState);
anyAppearance.blockEntityRenderer().ifPresent(state::setConstantRenderers);
}
int appearanceId = visualState.registryId();
int index = internalId - AbstractBlockManager.this.vanillaBlockStateCount;
AbstractBlockManager.this.immutableBlockStates[index] = state;
AbstractBlockManager.this.blockStateMappings[internalId] = appearanceId;
AbstractBlockManager.this.appearanceToRealState.computeIfAbsent(appearanceId, k -> new IntArrayList()).add(internalId);
AbstractBlockManager.this.tempVisualBlockStatesInUse.add(visualState);
AbstractBlockManager.this.tempVisualBlocksInUse.add(getBlockOwnerId(visualState));
AbstractBlockManager.this.applyPlatformSettings(state);
// generate mod assets
if (Config.generateModAssets()) {
AbstractBlockManager.this.modBlockStateOverrides.put(BlockManager.createCustomBlockKey(index), Optional.ofNullable(AbstractBlockManager.this.tempVanillaBlockStateModels[appearanceId])
.orElseGet(() -> {
// 如果未指定模型,说明复用原版模型?但是部分模型是多部位模型,无法使用变体解决问题
// 未来需要靠mod重构彻底解决问题
JsonObject json = new JsonObject();
json.addProperty("model", "minecraft:block/air");
return json;
}));
}
}
// 一定要到最后再绑定
customBlock.setBehavior(blockBehavior);
holder.bindValue(customBlock);
// 添加方块
AbstractBlockManager.this.byId.put(customBlock.id(), customBlock);
// 抛出次要警告
exceptionCollector.throwIfPresent();
}, () -> GsonHelper.get().toJson(section)));
}, () -> GsonHelper.get().toJson(section)));
}
@SuppressWarnings("unchecked")
@@ -316,7 +668,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
return properties;
}
private void arrangeModelForStateAndVerify(int registryId, Object modelOrModels) {
private void arrangeModelForStateAndVerify(BlockStateWrapper blockStateWrapper, Object modelOrModels) {
// 如果没有配置models
if (modelOrModels == null) {
return;
@@ -334,18 +686,19 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
}
}
// 拆分方块id与属性
String blockState = stateRegistryIdToStateSNBT(registryId);
Key blockId = Key.of(blockState.substring(blockState.indexOf('{') + 1, blockState.lastIndexOf('}')));
String propertyNBT = blockState.substring(blockState.indexOf('[') + 1, blockState.lastIndexOf(']'));
String blockState = blockStateWrapper.getAsString();
int firstIndex = blockState.indexOf('[');
Key blockId = firstIndex == -1 ? Key.of(blockState) : Key.of(blockState.substring(0, firstIndex));
String propertyNBT = firstIndex == -1 ? "" : blockState.substring(firstIndex + 1, blockState.lastIndexOf(']'));
// 结合variants
JsonElement combinedVariant = GsonHelper.combine(variants);
Map<String, JsonElement> overrideMap = AbstractBlockManager.this.blockStateOverrides.computeIfAbsent(blockId, k -> new HashMap<>());
AbstractBlockManager.this.tempVanillaBlockStateModels.put(registryId, combinedVariant);
JsonElement previous = overrideMap.get(propertyNBT);
if (previous != null && !previous.equals(combinedVariant)) {
throw new LocalizedResourceConfigException("warning.config.block.state.model.conflict", GsonHelper.get().toJson(combinedVariant), blockState, GsonHelper.get().toJson(previous));
}
overrideMap.put(propertyNBT, combinedVariant);
AbstractBlockManager.this.tempVanillaBlockStateModels[blockStateWrapper.registryId()] = combinedVariant;
}
private JsonObject parseAppearanceModelSectionAsJson(Map<String, Object> section) {
@@ -370,7 +723,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
}
// 从方块外观的state里获取其原版方块的state id
private int pluginFormattedBlockStateToRegistryId(String blockState) {
private BlockStateWrapper parsePluginFormattedBlockState(String blockState) {
// 五种合理情况
// minecraft:note_block:10
// note_block:10
@@ -381,7 +734,7 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
if (split.length >= 4) {
throw new LocalizedResourceConfigException("warning.config.block.state.invalid_vanilla", blockState);
}
int registryId;
BlockStateWrapper wrapper;
String stateOrId = split[split.length - 1];
boolean isId = false;
int arrangerIndex = 0;
@@ -401,14 +754,14 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
// 获取原版方块的id
Key block = split.length == 2 ? Key.of(split[0]) : Key.of(split[0], split[1]);
try {
List<Integer> arranger = blockAppearanceArranger.get(block);
List<BlockStateWrapper> arranger = AbstractBlockManager.this.blockStateArranger.get(block);
if (arranger == null) {
throw new LocalizedResourceConfigException("warning.config.block.state.unavailable_vanilla", blockState);
}
if (arrangerIndex >= arranger.size()) {
throw new LocalizedResourceConfigException("warning.config.block.state.invalid_vanilla_id", blockState, String.valueOf(arranger.size() - 1));
}
registryId = arranger.get(arrangerIndex);
wrapper = arranger.get(arrangerIndex);
} catch (NumberFormatException e) {
throw new LocalizedResourceConfigException("warning.config.block.state.invalid_vanilla", e, blockState);
}
@@ -418,9 +771,21 @@ public abstract class AbstractBlockManager extends AbstractModelGenerator implem
if (packedBlockState == null) {
throw new LocalizedResourceConfigException("warning.config.block.state.invalid_vanilla", blockState);
}
registryId = packedBlockState.registryId();
wrapper = packedBlockState;
}
return registryId;
return wrapper;
}
}
public boolean isVanillaBlockState(int id) {
return id < this.vanillaBlockStateCount && id >= 0;
}
public BlockParser blockParser() {
return blockParser;
}
public BlockStateMappingParser blockStateMappingParser() {
return blockStateMappingParser;
}
}

View File

@@ -0,0 +1,38 @@
package net.momirealms.craftengine.core.block;
public abstract class AbstractBlockStateWrapper implements BlockStateWrapper {
protected final Object blockState;
protected final int registryId;
protected AbstractBlockStateWrapper(Object blockState, int registryId) {
this.blockState = blockState;
this.registryId = registryId;
}
@Override
public Object literalObject() {
return this.blockState;
}
@Override
public int registryId() {
return this.registryId;
}
@Override
public String toString() {
return this.blockState.toString();
}
@Override
public final boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BlockStateWrapper that)) return false;
return this.registryId == that.registryId();
}
@Override
public int hashCode() {
return this.registryId;
}
}

View File

@@ -1,9 +1,6 @@
package net.momirealms.craftengine.core.block;
import com.google.common.collect.ImmutableMap;
import net.momirealms.craftengine.core.block.behavior.EmptyBlockBehavior;
import net.momirealms.craftengine.core.block.behavior.EntityBlockBehavior;
import net.momirealms.craftengine.core.block.parser.BlockNbtParser;
import net.momirealms.craftengine.core.block.properties.Property;
import net.momirealms.craftengine.core.item.context.BlockPlaceContext;
import net.momirealms.craftengine.core.loot.LootTable;
@@ -11,7 +8,6 @@ import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.context.event.EventTrigger;
import net.momirealms.craftengine.core.plugin.context.function.Function;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.registry.Holder;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.sparrow.nbt.CompoundTag;
@@ -23,87 +19,33 @@ import java.util.*;
import java.util.function.BiFunction;
public abstract class AbstractCustomBlock implements CustomBlock {
protected final Holder<CustomBlock> holder;
protected final Key id;
protected final Holder.Reference<CustomBlock> holder;
protected final BlockStateVariantProvider variantProvider;
protected final Map<String, Property<?>> properties;
protected final BlockBehavior behavior;
protected final BiFunction<BlockPlaceContext, ImmutableBlockState, ImmutableBlockState> placementFunction;
protected final ImmutableBlockState defaultState;
protected final Map<EventTrigger, List<Function<PlayerOptionalContext>>> events;
@Nullable
protected final LootTable<?> lootTable;
protected BlockBehavior behavior = EmptyBlockBehavior.INSTANCE;
protected AbstractCustomBlock(
@NotNull Key id,
@NotNull Holder.Reference<CustomBlock> holder,
@NotNull Map<String, Property<?>> properties,
@NotNull Map<String, BlockStateAppearance> appearances,
@NotNull Map<String, BlockStateVariant> variantMapper,
@NotNull BlockSettings settings,
@NotNull BlockStateVariantProvider variantProvider,
@NotNull Map<EventTrigger, List<Function<PlayerOptionalContext>>> events,
@Nullable List<Map<String, Object>> behaviorConfig,
@Nullable LootTable<?> lootTable
) {
holder.bindValue(this);
this.id = holder.key().location();
this.holder = holder;
this.id = id;
this.lootTable = lootTable;
this.properties = ImmutableMap.copyOf(properties);
this.events = events;
this.variantProvider = new BlockStateVariantProvider(holder, ImmutableBlockState::new, properties);
this.variantProvider = variantProvider;
this.defaultState = this.variantProvider.getDefaultState();
this.behavior = setupBehavior(behaviorConfig);
List<BiFunction<BlockPlaceContext, ImmutableBlockState, ImmutableBlockState>> placements = new ArrayList<>(4);
for (Map.Entry<String, Property<?>> propertyEntry : this.properties.entrySet()) {
for (Map.Entry<String, Property<?>> propertyEntry : this.variantProvider.properties().entrySet()) {
placements.add(Property.createStateForPlacement(propertyEntry.getKey(), propertyEntry.getValue()));
}
this.placementFunction = composite(placements);
EntityBlockBehavior entityBlockBehavior = this.behavior.getEntityBehavior();
boolean isEntityBlock = entityBlockBehavior != null;
for (Map.Entry<String, BlockStateVariant> entry : variantMapper.entrySet()) {
String nbtString = entry.getKey();
CompoundTag tag = BlockNbtParser.deserialize(this, nbtString);
if (tag == null) {
throw new LocalizedResourceConfigException("warning.config.block.state.property.invalid_format", nbtString);
}
List<ImmutableBlockState> possibleStates = this.getPossibleStates(tag);
if (possibleStates.size() != 1) {
throw new LocalizedResourceConfigException("warning.config.block.state.property.invalid_format", nbtString);
}
BlockStateVariant blockStateVariant = entry.getValue();
BlockStateAppearance blockStateAppearance = appearances.getOrDefault(blockStateVariant.appearance(), BlockStateAppearance.INVALID);
int stateId;
// This should never happen
if (blockStateAppearance.isInvalid()) {
stateId = appearances.values().iterator().next().stateRegistryId();
} else {
stateId = blockStateAppearance.stateRegistryId();
}
// Late init states
ImmutableBlockState state = possibleStates.getFirst();
state.setSettings(blockStateVariant.settings());
state.setVanillaBlockState(BlockRegistryMirror.stateByRegistryId(stateId));
state.setCustomBlockState(BlockRegistryMirror.stateByRegistryId(blockStateVariant.internalRegistryId()));
blockStateAppearance.blockEntityRenderer().ifPresent(state::setConstantRenderers);
}
// double check if there's any invalid state
for (ImmutableBlockState state : this.variantProvider().states()) {
state.setBehavior(this.behavior);
if (state.settings() == null) {
state.setSettings(settings);
}
if (isEntityBlock) {
state.setBlockEntityType(entityBlockBehavior.blockEntityType());
}
}
this.applyPlatformSettings();
}
protected BlockBehavior setupBehavior(List<Map<String, Object>> behaviorConfig) {
return EmptyBlockBehavior.INSTANCE;
}
private static BiFunction<BlockPlaceContext, ImmutableBlockState, ImmutableBlockState> composite(List<BiFunction<BlockPlaceContext, ImmutableBlockState, ImmutableBlockState>> placements) {
@@ -124,8 +66,6 @@ public abstract class AbstractCustomBlock implements CustomBlock {
};
}
protected abstract void applyPlatformSettings();
@Override
public @Nullable LootTable<?> lootTable() {
return this.lootTable;
@@ -150,25 +90,13 @@ public abstract class AbstractCustomBlock implements CustomBlock {
return this.id;
}
public void setBehavior(@Nullable BlockBehavior behavior) {
this.behavior = behavior;
}
@Override
public List<ImmutableBlockState> getPossibleStates(CompoundTag nbt) {
List<ImmutableBlockState> tempStates = new ArrayList<>();
tempStates.add(defaultState());
for (Property<?> property : this.variantProvider.getDefaultState().getProperties()) {
Tag value = nbt.get(property.name());
if (value != null) {
tempStates.replaceAll(immutableBlockState -> ImmutableBlockState.with(immutableBlockState, property, property.unpack(value)));
} else {
List<ImmutableBlockState> newStates = new ArrayList<>();
for (ImmutableBlockState state : tempStates) {
for (Object possibleValue : property.possibleValues()) {
newStates.add(ImmutableBlockState.with(state, property, possibleValue));
}
}
tempStates = newStates;
}
}
return tempStates;
return this.variantProvider.getPossibleStates(nbt);
}
@Override
@@ -189,12 +117,12 @@ public abstract class AbstractCustomBlock implements CustomBlock {
@Override
public @Nullable Property<?> getProperty(String name) {
return this.properties.get(name);
return this.variantProvider.getProperty(name);
}
@Override
public @NotNull Collection<Property<?>> properties() {
return this.properties.values();
return this.variantProvider.properties().values();
}
@Override

View File

@@ -0,0 +1,117 @@
package net.momirealms.craftengine.core.block;
import net.momirealms.craftengine.core.pack.allocator.BlockStateCandidate;
import net.momirealms.craftengine.core.util.Key;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.function.Predicate;
public enum AutoStateGroup {
LEAVES("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, BlockKeys.CHERRY_LEAVES, BlockKeys.PALE_OAK_LEAVES, BlockKeys.AZALEA_LEAVES, BlockKeys.FLOWERING_AZALEA_LEAVES),
(w) -> !(boolean) w.getProperty("waterlogged")
),
WATERLOGGED_LEAVES(
"waterlogged_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, BlockKeys.CHERRY_LEAVES, BlockKeys.PALE_OAK_LEAVES, BlockKeys.AZALEA_LEAVES, BlockKeys.FLOWERING_AZALEA_LEAVES),
(w) -> w.getProperty("waterlogged")
),
LOWER_TRIPWIRE("lower_tripwire", Set.of(BlockKeys.TRIPWIRE), (w) -> w.getProperty("attached")),
HIGHER_TRIPWIRE("higher_tripwire", Set.of(BlockKeys.TRIPWIRE), (w) -> !(boolean) w.getProperty("attached")),
NOTE_BLOCK("note_block", Set.of(BlockKeys.NOTE_BLOCK), (w) -> true),
BROWN_MUSHROOM("brown_mushroom", Set.of(BlockKeys.BROWN_MUSHROOM_BLOCK), (w) -> true),
RED_MUSHROOM("red_mushroom", Set.of(BlockKeys.RED_MUSHROOM_BLOCK), (w) -> true),
MUSHROOM_STEM("mushroom_stem", Set.of(BlockKeys.MUSHROOM_STEM), (w) -> true),
TRIPWIRE("tripwire", Set.of(BlockKeys.TRIPWIRE), (w) -> true),
SUGAR_CANE("sugar_cane", Set.of(BlockKeys.SUGAR_CANE), (w) -> true),
CACTUS("cactus", Set.of(BlockKeys.CACTUS), (w) -> true),
SAPLING("sapling", Set.of(BlockKeys.OAK_SAPLING, BlockKeys.SPRUCE_SAPLING, BlockKeys.BIRCH_SAPLING, BlockKeys.JUNGLE_SAPLING, BlockKeys.ACACIA_SAPLING, BlockKeys.DARK_OAK_SAPLING, BlockKeys.CHERRY_SAPLING, BlockKeys.PALE_OAK_SAPLING), (w) -> true),
MUSHROOM("mushroom", Set.of(BlockKeys.BROWN_MUSHROOM_BLOCK, BlockKeys.RED_MUSHROOM_BLOCK, BlockKeys.MUSHROOM_STEM), (w) -> true),
SOLID("solid", Set.of(BlockKeys.BROWN_MUSHROOM_BLOCK, BlockKeys.RED_MUSHROOM_BLOCK, BlockKeys.MUSHROOM_STEM, BlockKeys.NOTE_BLOCK), (w) -> true);
private final Set<Key> blocks;
private final String id;
private final Predicate<BlockStateWrapper> predicate;
private final List<BlockStateCandidate> candidates = new ArrayList<>();
private int pointer;
AutoStateGroup(String id, Set<Key> blocks, Predicate<BlockStateWrapper> predicate) {
this.id = id;
this.blocks = blocks;
this.predicate = predicate;
}
public void reset() {
this.pointer = 0;
this.candidates.clear();
}
public void addCandidate(@NotNull BlockStateCandidate candidate) {
this.candidates.add(candidate);
}
public int candidateCount() {
return candidates.size();
}
@Nullable
public BlockStateCandidate findNextCandidate() {
while (this.pointer < this.candidates.size()) {
final BlockStateCandidate state = this.candidates.get(this.pointer);
if (!state.isUsed()) {
return state;
}
this.pointer++;
}
return null;
}
public boolean test(BlockStateWrapper state) {
if (!this.blocks.contains(state.ownerId())) {
return false;
}
return this.predicate.test(state);
}
public Set<Key> blocks() {
return blocks;
}
public String id() {
return id;
}
private static final Map<String, AutoStateGroup> BY_ID = new HashMap<>();
private static final Map<Key, List<AutoStateGroup>> BY_BLOCKS = new HashMap<>();
static {
for (AutoStateGroup group : AutoStateGroup.values()) {
BY_ID.put(group.id(), group);
BY_ID.put(group.id().toUpperCase(Locale.ROOT), group);
for (Key key : group.blocks) {
BY_BLOCKS.computeIfAbsent(key, k -> new ArrayList<>(4)).add(group);
}
}
}
@Nullable
public static AutoStateGroup byId(String id) {
return BY_ID.get(id);
}
public static List<AutoStateGroup> findGroups(BlockStateWrapper wrapper) {
return findGroups(wrapper.ownerId(), wrapper);
}
public static List<AutoStateGroup> findGroups(Key id, BlockStateWrapper wrapper) {
List<AutoStateGroup> groups = BY_BLOCKS.get(id);
if (groups == null) return Collections.emptyList();
List<AutoStateGroup> result = new ArrayList<>(groups.size());
for (AutoStateGroup group : groups) {
if (group.predicate.test(wrapper)) result.add(group);
}
return result;
}
}

View File

@@ -55,18 +55,18 @@ public abstract class BlockBehavior {
superMethod.call();
}
// ServerLevel level, BlockPos pos, RandomSource random
// BlockState state, ServerLevel level, BlockPos pos, RandomSource random
public void tick(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
superMethod.call();
}
// ServerLevel level, BlockPos pos, RandomSource random
// BlockState state, ServerLevel level, BlockPos pos, RandomSource random
public void randomTick(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
superMethod.call();
}
// 1.20-1.20.4 BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify, UseOnContext context
// 1.20.5+ Level level, BlockPos pos, BlockState oldState, boolean movedByPiston
// 1.20.5+ BlockState state, Level level, BlockPos pos, BlockState oldState, boolean movedByPiston
public void onPlace(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
superMethod.call();
}
@@ -95,12 +95,12 @@ public abstract class BlockBehavior {
return false;
}
//BlockState state
// BlockState state
public boolean hasAnalogOutputSignal(Object thisBlock, Object[] args) throws Exception {
return false;
}
//BlockState state, Level level, BlockPos pos
// BlockState state, Level level, BlockPos pos
public int getAnalogOutputSignal(Object thisBlock, Object[] args) throws Exception {
return 0;
}
@@ -142,6 +142,7 @@ public abstract class BlockBehavior {
// 1.20-1.21.4 BlockState state, Level level, BlockPos pos, Entity entity
// 1.21.5+ BlockState state, Level level, BlockPos pos, Entity entity, InsideBlockEffectApplier effectApplier
// 1.21.10+ BlockState state, Level level, BlockPos pos, Entity entity, InsideBlockEffectApplier effectApplier, boolean flag
public void entityInside(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
}
@@ -178,6 +179,14 @@ public abstract class BlockBehavior {
public void spawnAfterBreak(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
}
// Level level, BlockPos pos, BlockState state, Entity entity
public void stepOn(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
}
// Level level, BlockState state, BlockHitResult hit, Projectile projectile
public void onProjectileHit(Object thisBlock, Object[] args, Callable<Object> superMethod) throws Exception {
}
public ImmutableBlockState updateStateForPlacement(BlockPlaceContext context, ImmutableBlockState state) {
return state;
}
@@ -211,4 +220,4 @@ public abstract class BlockBehavior {
}
public abstract CustomBlock block();
}
}

View File

@@ -2,9 +2,12 @@ package net.momirealms.craftengine.core.block;
import net.momirealms.craftengine.core.util.Key;
import java.util.List;
public final class BlockKeys {
private BlockKeys() {}
public static final Key SUGAR_CANE = Key.of("minecraft:sugar_cane");
public static final Key NOTE_BLOCK = Key.of("minecraft:note_block");
public static final Key TRIPWIRE = Key.of("minecraft:tripwire");
public static final Key CRAFTING_TABLE = Key.of("minecraft:crafting_table");
@@ -243,4 +246,58 @@ public final class BlockKeys {
public static final Key BAMBOO_WALL_HANGING_SIGN = Key.of("minecraft:bamboo_wall_hanging_sign");
public static final Key CACTUS = Key.of("minecraft:cactus");
public static final Key BROWN_MUSHROOM_BLOCK = Key.of("minecraft:brown_mushroom_block");
public static final Key RED_MUSHROOM_BLOCK = Key.of("minecraft:red_mushroom_block");
public static final Key MUSHROOM_STEM = Key.of("minecraft:mushroom_stem");
public static final Key OAK_LEAVES = Key.of("minecraft:oak_leaves");
public static final Key SPRUCE_LEAVES = Key.of("minecraft:spruce_leaves");
public static final Key BIRCH_LEAVES = Key.of("minecraft:birch_leaves");
public static final Key JUNGLE_LEAVES = Key.of("minecraft:jungle_leaves");
public static final Key ACACIA_LEAVES = Key.of("minecraft:acacia_leaves");
public static final Key DARK_OAK_LEAVES = Key.of("minecraft:dark_oak_leaves");
public static final Key MANGROVE_LEAVES = Key.of("minecraft:mangrove_leaves");
public static final Key CHERRY_LEAVES = Key.of("minecraft:cherry_leaves");
public static final Key PALE_OAK_LEAVES = Key.of("minecraft:pale_oak_leaves");
public static final Key AZALEA_LEAVES = Key.of("minecraft:azalea_leaves");
public static final Key FLOWERING_AZALEA_LEAVES = Key.of("minecraft:flowering_azalea_leaves");
public static final Key OAK_SAPLING = Key.of("minecraft:oak_sapling");
public static final Key SPRUCE_SAPLING = Key.of("minecraft:spruce_sapling");
public static final Key BIRCH_SAPLING = Key.of("minecraft:birch_sapling");
public static final Key JUNGLE_SAPLING = Key.of("minecraft:jungle_sapling");
public static final Key DARK_OAK_SAPLING = Key.of("minecraft:dark_oak_sapling");
public static final Key ACACIA_SAPLING = Key.of("minecraft:acacia_sapling");
public static final Key CHERRY_SAPLING = Key.of("minecraft:cherry_sapling");
public static final Key PALE_OAK_SAPLING = Key.of("minecraft:pale_oak_sapling");
public static final List<Key> WOODEN_TRAPDOORS = List.of(OAK_TRAPDOOR, SPRUCE_TRAPDOOR, BIRCH_TRAPDOOR,
ACACIA_TRAPDOOR, PALE_OAK_TRAPDOOR, DARK_OAK_TRAPDOOR, MANGROVE_TRAPDOOR, JUNGLE_TRAPDOOR);
public static final List<Key> CHERRY_TRAPDOORS = List.of(CHERRY_TRAPDOOR);
public static final List<Key> BAMBOO_TRAPDOORS = List.of(BAMBOO_TRAPDOOR);
public static final List<Key> NETHER_TRAPDOORS = List.of(WARPED_TRAPDOOR, CRIMSON_TRAPDOOR);
public static final List<Key> COPPER_TRAPDOORS = List.of(COPPER_TRAPDOOR, EXPOSED_COPPER_TRAPDOOR, WEATHERED_COPPER_TRAPDOOR, OXIDIZED_COPPER_DOOR,
WAXED_COPPER_TRAPDOOR, WAXED_EXPOSED_COPPER_TRAPDOOR, WAXED_WEATHERED_COPPER_TRAPDOOR, WAXED_OXIDIZED_COPPER_TRAPDOOR);
public static final List<Key> WOODEN_DOORS = List.of(OAK_DOOR, SPRUCE_DOOR, BIRCH_DOOR,
ACACIA_DOOR, PALE_OAK_DOOR, DARK_OAK_DOOR, MANGROVE_DOOR, JUNGLE_DOOR);
public static final List<Key> CHERRY_DOORS = List.of(CHERRY_DOOR);
public static final List<Key> BAMBOO_DOORS = List.of(BAMBOO_DOOR);
public static final List<Key> NETHER_DOORS = List.of(WARPED_DOOR, CRIMSON_DOOR);
public static final List<Key> COPPER_DOORS = List.of(COPPER_DOOR, EXPOSED_COPPER_DOOR, WEATHERED_COPPER_DOOR, OXIDIZED_COPPER_DOOR,
WAXED_COPPER_DOOR, WAXED_EXPOSED_COPPER_DOOR, WAXED_WEATHERED_COPPER_DOOR, WAXED_OXIDIZED_COPPER_DOOR);
public static final List<Key> WOODEN_FENCE_GATES = List.of(OAK_FENCE_GATE, SPRUCE_FENCE_GATE, BIRCH_FENCE_GATE,
ACACIA_FENCE_GATE, PALE_OAK_FENCE_GATE, DARK_OAK_FENCE_GATE, MANGROVE_FENCE_GATE, JUNGLE_FENCE_GATE);
public static final List<Key> CHERRY_FENCE_GATES = List.of(CHERRY_FENCE_GATE);
public static final List<Key> BAMBOO_FENCE_GATES = List.of(BAMBOO_FENCE_GATE);
public static final List<Key> NETHER_FENCE_GATES = List.of(WARPED_FENCE_GATE, CRIMSON_FENCE_GATE);
public static final List<Key> WOODEN_BUTTONS = List.of(OAK_BUTTON, SPRUCE_BUTTON, BIRCH_BUTTON, JUNGLE_BUTTON,
ACACIA_BUTTON, DARK_OAK_BUTTON, PALE_OAK_BUTTON, MANGROVE_BUTTON);
public static final List<Key> CHERRY_BUTTONS = List.of(CHERRY_BUTTON);
public static final List<Key> BAMBOO_BUTTONS = List.of(BAMBOO_BUTTON);
public static final List<Key> NETHER_BUTTONS = List.of(CRIMSON_BUTTON, WARPED_BUTTON);
public static final List<Key> STONE_BUTTONS = List.of(STONE_BUTTON, POLISHED_BLACKSTONE_BUTTON);
}

View File

@@ -16,7 +16,7 @@ import java.util.Optional;
public interface BlockManager extends Manageable, ModelGenerator {
ConfigParser parser();
ConfigParser[] parsers();
Collection<ModelGeneration> modelsToGenerate();
@@ -35,9 +35,7 @@ public interface BlockManager extends Manageable, ModelGenerator {
Collection<Suggestion> cachedSuggestions();
Map<Key, Key> soundMapper();
int availableAppearances(Key blockType);
Map<Key, Key> soundReplacements();
Key getBlockOwnerId(BlockStateWrapper state);
@@ -49,4 +47,11 @@ public interface BlockManager extends Manageable, ModelGenerator {
@Nullable
BlockStateWrapper createBlockState(String blockState);
@Nullable
BlockStateWrapper createVanillaBlockState(String blockState);
static Key createCustomBlockKey(int id) {
return Key.of("craftengine", "custom_" + id);
}
}

View File

@@ -5,14 +5,14 @@ public final class BlockRegistryMirror {
private static BlockStateWrapper stoneState;
public static void init(BlockStateWrapper[] states, BlockStateWrapper state) {
if (blockStates != null) throw new IllegalStateException("block states are already set");
if (blockStates != null) throw new IllegalStateException("block states have already been set");
blockStates = states;
stoneState = state;
}
public static BlockStateWrapper stateByRegistryId(int vanillaId) {
if (vanillaId < 0) return stoneState;
return blockStates[vanillaId];
public static BlockStateWrapper byId(int stateId) {
if (stateId < 0) return stoneState;
return blockStates[stateId];
}
public static int size() {

View File

@@ -1,7 +1,6 @@
package net.momirealms.craftengine.core.block;
import net.momirealms.craftengine.core.sound.SoundData;
import net.momirealms.craftengine.core.util.Key;
import java.util.Map;
@@ -15,25 +14,20 @@ public final class BlockSounds {
Land 0.3 1
Destroy 1 1
*/
public static final SoundData EMPTY_SOUND = new SoundData(Key.of("minecraft:intentionally_empty"), SoundData.SoundValue.FIXED_1, SoundData.SoundValue.FIXED_1);
public static final BlockSounds EMPTY = new BlockSounds(EMPTY_SOUND, EMPTY_SOUND, EMPTY_SOUND, EMPTY_SOUND, EMPTY_SOUND, EMPTY_SOUND, EMPTY_SOUND);
public static final BlockSounds EMPTY = new BlockSounds(SoundData.EMPTY, SoundData.EMPTY, SoundData.EMPTY, SoundData.EMPTY, SoundData.EMPTY);
private final SoundData breakSound;
private final SoundData stepSound;
private final SoundData placeSound;
private final SoundData hitSound;
private final SoundData fallSound;
private final SoundData landSound;
private final SoundData destroySound;
public BlockSounds(SoundData breakSound, SoundData stepSound, SoundData placeSound, SoundData hitSound, SoundData fallSound, SoundData landSound, SoundData destroySound) {
public BlockSounds(SoundData breakSound, SoundData stepSound, SoundData placeSound, SoundData hitSound, SoundData fallSound) {
this.breakSound = breakSound;
this.stepSound = stepSound;
this.placeSound = placeSound;
this.hitSound = hitSound;
this.fallSound = fallSound;
this.landSound = landSound;
this.destroySound = destroySound;
}
public static BlockSounds fromMap(Map<String, Object> map) {
@@ -43,16 +37,10 @@ public final class BlockSounds {
SoundData.create(map.getOrDefault("step", "minecraft:intentionally_empty"), SoundData.SoundValue.FIXED_0_15, SoundData.SoundValue.FIXED_1),
SoundData.create(map.getOrDefault("place", "minecraft:intentionally_empty"), SoundData.SoundValue.FIXED_1, SoundData.SoundValue.FIXED_0_8),
SoundData.create(map.getOrDefault("hit", "minecraft:intentionally_empty"), SoundData.SoundValue.FIXED_0_5, SoundData.SoundValue.FIXED_0_5),
SoundData.create(map.getOrDefault("fall", "minecraft:intentionally_empty"), SoundData.SoundValue.FIXED_0_5, SoundData.SoundValue.FIXED_0_75),
SoundData.create(map.getOrDefault("land", "minecraft:intentionally_empty"), SoundData.SoundValue.FIXED_0_3, SoundData.SoundValue.FIXED_1),
SoundData.create(map.getOrDefault("destroy", "minecraft:intentionally_empty"), SoundData.SoundValue.FIXED_1, SoundData.SoundValue.FIXED_1)
SoundData.create(map.getOrDefault("fall", "minecraft:intentionally_empty"), SoundData.SoundValue.FIXED_0_5, SoundData.SoundValue.FIXED_0_75)
);
}
public SoundData destroySound() {
return destroySound;
}
public SoundData breakSound() {
return breakSound;
}
@@ -69,10 +57,6 @@ public final class BlockSounds {
return hitSound;
}
public SoundData landSound() {
return landSound;
}
public SoundData fallSound() {
return fallSound;
}

View File

@@ -5,10 +5,5 @@ import net.momirealms.craftengine.core.block.entity.render.element.BlockEntityEl
import java.util.Optional;
public record BlockStateAppearance(int stateRegistryId, Optional<BlockEntityElementConfig<? extends BlockEntityElement>[]> blockEntityRenderer) {
public static final BlockStateAppearance INVALID = new BlockStateAppearance(-1, Optional.empty());
public boolean isInvalid() {
return this.stateRegistryId < 0;
}
public record BlockStateAppearance(BlockStateWrapper blockState, Optional<BlockEntityElementConfig<? extends BlockEntityElement>[]> blockEntityRenderer) {
}

View File

@@ -1,131 +0,0 @@
package net.momirealms.craftengine.core.block;
import it.unimi.dsi.fastutil.objects.Reference2ObjectArrayMap;
import net.momirealms.craftengine.core.block.properties.Property;
import net.momirealms.craftengine.core.registry.Holder;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.stream.Collectors;
public class BlockStateHolder {
protected final Holder<CustomBlock> owner;
private final Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap;
private Map<Property<?>, ImmutableBlockState[]> withMap;
public BlockStateHolder(Holder<CustomBlock> owner, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap) {
this.owner = owner;
this.propertyMap = new Reference2ObjectArrayMap<>(propertyMap);
}
public Holder<CustomBlock> owner() {
return this.owner;
}
public <T extends Comparable<T>> ImmutableBlockState cycle(Property<T> property) {
T currentValue = get(property);
List<T> values = property.possibleValues();
return with(property, getNextValue(values, currentValue));
}
protected static <T> T getNextValue(List<T> values, T currentValue) {
int index = values.indexOf(currentValue);
if (index == -1) {
throw new IllegalArgumentException("Current value not found in possible values");
}
return values.get((index + 1) % values.size());
}
@Override
public String toString() {
if (this.propertyMap.isEmpty()) {
return this.owner.value().id().toString();
}
return this.owner.value().id() + "[" + getPropertiesAsString() + "]";
}
public String getPropertiesAsString() {
return this.propertyMap.entrySet().stream()
.map(entry -> {
Property<?> property = entry.getKey();
return property.name() + "=" + Property.formatValue(property, entry.getValue());
})
.collect(Collectors.joining(","));
}
public Collection<Property<?>> getProperties() {
return Collections.unmodifiableSet(this.propertyMap.keySet());
}
public <T extends Comparable<T>> boolean contains(Property<T> property) {
return this.propertyMap.containsKey(property);
}
public <T extends Comparable<T>> T get(Property<T> property) {
T value = getNullable(property);
if (value == null) {
throw new IllegalArgumentException("Property " + property + " not found in " + this.owner.value().id());
}
return value;
}
public <T extends Comparable<T>> T get(Property<T> property, T fallback) {
return Objects.requireNonNullElse(getNullable(property), fallback);
}
@Nullable
public <T extends Comparable<T>> T getNullable(Property<T> property) {
Comparable<?> value = this.propertyMap.get(property);
return value != null ? property.valueClass().cast(value) : null;
}
public <T extends Comparable<T>, V extends T> ImmutableBlockState with(Property<T> property, V value) {
if (!this.propertyMap.containsKey(property)) {
throw new IllegalArgumentException("Property " + property + " not found in " + this.owner.value().id());
}
return withInternal(property, value);
}
private <T extends Comparable<T>, V extends T> ImmutableBlockState withInternal(Property<T> property, V newValue) {
if (newValue.equals(this.propertyMap.get(property))) {
return (ImmutableBlockState) this;
}
int index = property.indexOf(newValue);
if (index == -1) {
throw new IllegalArgumentException("Invalid value " + newValue + " for property " + property);
}
return this.withMap.get(property)[index];
}
public void createWithMap(Map<Map<Property<?>, Comparable<?>>, ImmutableBlockState> states) {
if (this.withMap != null) {
throw new IllegalStateException("WithMap already initialized");
}
Reference2ObjectArrayMap<Property<?>, ImmutableBlockState[]> map = new Reference2ObjectArrayMap<>(this.propertyMap.size());
for (Property<?> property : this.propertyMap.keySet()) {
ImmutableBlockState[] statesArray = property.possibleValues().stream()
.map(value -> {
Map<Property<?>, Comparable<?>> testMap = new Reference2ObjectArrayMap<>(this.propertyMap);
testMap.put(property, value);
ImmutableBlockState state = states.get(testMap);
if (state == null) {
throw new IllegalStateException("Missing state for " + testMap);
}
return state;
})
.toArray(ImmutableBlockState[]::new);
map.put(property, statesArray);
}
this.withMap = Map.copyOf(map);
}
public Map<Property<?>, Comparable<?>> propertyEntries() {
return Collections.unmodifiableMap(this.propertyMap);
}
}

View File

@@ -1,25 +0,0 @@
package net.momirealms.craftengine.core.block;
public class BlockStateVariant {
private final String appearance;
private final BlockSettings settings;
private final int internalId;
public BlockStateVariant(String appearance, BlockSettings settings, int internalId) {
this.appearance = appearance;
this.settings = settings;
this.internalId = internalId;
}
public String appearance() {
return appearance;
}
public BlockSettings settings() {
return settings;
}
public int internalRegistryId() {
return internalId;
}
}

View File

@@ -8,6 +8,8 @@ import it.unimi.dsi.fastutil.objects.Reference2ObjectArrayMap;
import net.momirealms.craftengine.core.block.properties.Property;
import net.momirealms.craftengine.core.registry.Holder;
import net.momirealms.craftengine.core.util.Pair;
import net.momirealms.sparrow.nbt.CompoundTag;
import net.momirealms.sparrow.nbt.Tag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -23,7 +25,7 @@ public final class BlockStateVariantProvider {
private final ImmutableList<ImmutableBlockState> states;
private final Holder<CustomBlock> owner;
public BlockStateVariantProvider(Holder<CustomBlock> owner, Factory<Holder<CustomBlock>, ImmutableBlockState> factory, Map<String, Property<?>> propertiesMap) {
public BlockStateVariantProvider(Holder.Reference<CustomBlock> owner, Factory<Holder.Reference<CustomBlock>, ImmutableBlockState> factory, Map<String, Property<?>> propertiesMap) {
this.owner = owner;
this.properties = ImmutableSortedMap.copyOf(propertiesMap);
@@ -59,6 +61,27 @@ public final class BlockStateVariantProvider {
this.states = ImmutableList.copyOf(list);
}
public List<ImmutableBlockState> getPossibleStates(CompoundTag nbt) {
List<ImmutableBlockState> tempStates = new ArrayList<>();
ImmutableBlockState defaultState = getDefaultState();
tempStates.add(defaultState);
for (Property<?> property : defaultState.getProperties()) {
Tag value = nbt.get(property.name());
if (value != null) {
tempStates.replaceAll(immutableBlockState -> ImmutableBlockState.with(immutableBlockState, property, property.unpack(value)));
} else {
List<ImmutableBlockState> newStates = new ArrayList<>();
for (ImmutableBlockState state : tempStates) {
for (Object possibleValue : property.possibleValues()) {
newStates.add(ImmutableBlockState.with(state, property, possibleValue));
}
}
tempStates = newStates;
}
}
return tempStates;
}
public interface Factory<O, S> {
S create(O owner, Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap);
}
@@ -80,6 +103,11 @@ public final class BlockStateVariantProvider {
return this.owner;
}
@NotNull
public ImmutableSortedMap<String, Property<?>> properties() {
return this.properties;
}
@Nullable
public Property<?> getProperty(String name) {
return this.properties.get(name);

View File

@@ -1,8 +1,44 @@
package net.momirealms.craftengine.core.block;
public interface BlockStateWrapper {
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.sparrow.nbt.*;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
public interface BlockStateWrapper extends Comparable<BlockStateWrapper> {
Object literalObject();
int registryId();
Key ownerId();
<T> T getProperty(String propertyName);
boolean hasProperty(String propertyName);
BlockStateWrapper withProperty(String propertyName, String propertyValue);
String getAsString();
@Override
default int compareTo(@NotNull BlockStateWrapper o) {
return Integer.compare(registryId(), o.registryId());
}
default BlockStateWrapper withProperties(CompoundTag properties) {
BlockStateWrapper result = this;
for (Map.Entry<String, Tag> entry : properties.entrySet()) {
Tag value = entry.getValue();
if (value instanceof StringTag stringTag) {
result = result.withProperty(entry.getKey(), stringTag.getAsString());
} else if (value instanceof IntTag intTag) {
result = result.withProperty(entry.getKey(), String.valueOf(intTag.getAsInt()));
} else if (value instanceof ByteTag byteTag) {
result = result.withProperty(entry.getKey(), String.valueOf(byteTag.booleanValue()));
}
}
return result;
}
}

View File

@@ -5,7 +5,6 @@ import net.momirealms.craftengine.core.item.context.BlockPlaceContext;
import net.momirealms.craftengine.core.loot.LootTable;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.context.event.EventTrigger;
import net.momirealms.craftengine.core.plugin.context.function.Function;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.sparrow.nbt.CompoundTag;
import org.jetbrains.annotations.NotNull;
@@ -13,7 +12,6 @@ import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public interface CustomBlock {
@@ -40,23 +38,4 @@ public interface CustomBlock {
ImmutableBlockState getStateForPlacement(BlockPlaceContext context);
void setPlacedBy(BlockPlaceContext context, ImmutableBlockState state);
interface Builder {
Builder events(Map<EventTrigger, List<Function<PlayerOptionalContext>>> events);
Builder appearances(Map<String, BlockStateAppearance> appearances);
Builder behavior(List<Map<String, Object>> behavior);
Builder lootTable(LootTable<?> lootTable);
Builder properties(Map<String, Property<?>> properties);
Builder settings(BlockSettings settings);
Builder variantMapper(Map<String, BlockStateVariant> variantMapper);
@NotNull CustomBlock build();
}
}

View File

@@ -32,9 +32,11 @@ public interface DelegatingBlock {
*/
ObjectHolder<BlockBehavior> behaviorDelegate();
// 其实是错误的做法
@Deprecated
boolean isNoteBlock();
// 其实是错误的做法
@Deprecated
boolean isTripwire();
}

View File

@@ -1,22 +1,32 @@
package net.momirealms.craftengine.core.block;
import net.momirealms.craftengine.core.block.behavior.EmptyBlockBehavior;
import net.momirealms.craftengine.core.registry.BuiltInRegistries;
import net.momirealms.craftengine.core.registry.Holder;
import net.momirealms.craftengine.core.registry.WritableRegistry;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.ResourceKey;
import java.util.List;
import java.util.Map;
public final class EmptyBlock extends AbstractCustomBlock {
public static EmptyBlock INSTANCE;
public static ImmutableBlockState STATE;
public static final EmptyBlock INSTANCE;
public static final ImmutableBlockState STATE;
public EmptyBlock(Key id, Holder.Reference<CustomBlock> holder) {
super(id, holder, Map.of(), Map.of(), Map.of(), BlockSettings.of(), Map.of(), List.of(), null);
INSTANCE = this;
STATE = defaultState();
static {
Holder.Reference<CustomBlock> holder = ((WritableRegistry<CustomBlock>) BuiltInRegistries.BLOCK)
.registerForHolder(ResourceKey.create(BuiltInRegistries.BLOCK.key().location(), Key.withDefaultNamespace("empty")));
INSTANCE = new EmptyBlock(holder);
holder.bindValue(INSTANCE);
STATE = INSTANCE.defaultState();
STATE.setSettings(BlockSettings.of());
STATE.setBehavior(EmptyBlockBehavior.INSTANCE);
}
@Override
protected void applyPlatformSettings() {
private EmptyBlock(Holder.Reference<CustomBlock> holder) {
super(holder, new BlockStateVariantProvider(holder, ImmutableBlockState::new, Map.of()), Map.of(), null);
}
public static void initialize() {
}
}

View File

@@ -22,9 +22,14 @@ import net.momirealms.sparrow.nbt.Tag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
public final class ImmutableBlockState {
private final Holder.Reference<CustomBlock> owner;
private final Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap;
private Map<Property<?>, ImmutableBlockState[]> withMap;
public final class ImmutableBlockState extends BlockStateHolder {
private CompoundTag tag;
private BlockStateWrapper customBlockState;
private BlockStateWrapper vanillaBlockState;
@@ -35,10 +40,11 @@ public final class ImmutableBlockState extends BlockStateHolder {
private BlockEntityElementConfig<? extends BlockEntityElement>[] renderers;
ImmutableBlockState(
Holder<CustomBlock> owner,
Holder.Reference<CustomBlock> owner,
Reference2ObjectArrayMap<Property<?>, Comparable<?>> propertyMap
) {
super(owner, propertyMap);
this.owner = owner;
this.propertyMap = new Reference2ObjectArrayMap<>(propertyMap);
}
public BlockBehavior behavior() {
@@ -101,24 +107,6 @@ public final class ImmutableBlockState extends BlockStateHolder {
this.vanillaBlockState = vanillaBlockState;
}
public CompoundTag propertiesNbt() {
CompoundTag properties = new CompoundTag();
for (Property<?> property : getProperties()) {
Comparable<?> value = get(property);
if (value != null) {
properties.put(property.name(), pack(property, value));
continue;
}
properties.put(property.name(), pack(property, property.defaultValue()));
}
return properties;
}
@SuppressWarnings("unchecked")
public static <T extends Comparable<T>> Tag pack(Property<T> property, Object value) {
return property.pack((T) value);
}
public CompoundTag getNbtToSave() {
if (this.tag == null) {
this.tag = toNbtToSave(propertiesNbt());
@@ -129,7 +117,7 @@ public final class ImmutableBlockState extends BlockStateHolder {
public CompoundTag toNbtToSave(CompoundTag properties) {
CompoundTag tag = new CompoundTag();
tag.put("properties", properties);
tag.put("id", NBT.createString(this.owner.value().id().asString()));
tag.put("id", NBT.createString(this.owner.key().location().asString()));
return tag;
}
@@ -137,11 +125,6 @@ public final class ImmutableBlockState extends BlockStateHolder {
this.tag = tag;
}
@SuppressWarnings("unchecked")
public static <T extends Comparable<T>> ImmutableBlockState with(ImmutableBlockState state, Property<T> property, Object value) {
return state.with(property, (T) value);
}
@SuppressWarnings("unchecked")
public List<Item<Object>> getDrops(@NotNull ContextHolder.Builder builder, @NotNull World world, @Nullable Player player) {
CustomBlock block = this.owner.value();
@@ -152,9 +135,158 @@ public final class ImmutableBlockState extends BlockStateHolder {
}
@SuppressWarnings("unchecked")
public <T extends BlockEntity> BlockEntityTicker<T> createBlockEntityTicker(CEWorld world, BlockEntityType<? extends BlockEntity> type) {
public <T extends BlockEntity> BlockEntityTicker<T> createSyncBlockEntityTicker(CEWorld world, BlockEntityType<? extends BlockEntity> type) {
EntityBlockBehavior blockBehavior = this.behavior.getEntityBehavior();
if (blockBehavior == null) return null;
return (BlockEntityTicker<T>) blockBehavior.createBlockEntityTicker(world, this, type);
return (BlockEntityTicker<T>) blockBehavior.createSyncBlockEntityTicker(world, this, type);
}
@SuppressWarnings("unchecked")
public <T extends BlockEntity> BlockEntityTicker<T> createAsyncBlockEntityTicker(CEWorld world, BlockEntityType<? extends BlockEntity> type) {
EntityBlockBehavior blockBehavior = this.behavior.getEntityBehavior();
if (blockBehavior == null) return null;
return (BlockEntityTicker<T>) blockBehavior.createAsyncBlockEntityTicker(world, this, type);
}
public Holder<CustomBlock> owner() {
return this.owner;
}
public <T extends Comparable<T>> ImmutableBlockState cycle(Property<T> property) {
T currentValue = get(property);
List<T> values = property.possibleValues();
return with(property, getNextValue(values, currentValue));
}
private static <T> T getNextValue(List<T> values, T currentValue) {
int index = values.indexOf(currentValue);
if (index == -1) {
throw new IllegalArgumentException("Current value not found in possible values");
}
return values.get((index + 1) % values.size());
}
@Override
public String toString() {
if (this.propertyMap.isEmpty()) {
return this.owner.key().location().toString();
}
return this.owner.key().location() + "[" + getPropertiesAsString() + "]";
}
public String getPropertiesAsString() {
return this.propertyMap.entrySet().stream()
.map(entry -> {
Property<?> property = entry.getKey();
return property.name() + "=" + Property.formatValue(property, entry.getValue());
})
.collect(Collectors.joining(","));
}
public Collection<Property<?>> getProperties() {
return Collections.unmodifiableSet(this.propertyMap.keySet());
}
public <T extends Comparable<T>> boolean contains(Property<T> property) {
return this.propertyMap.containsKey(property);
}
public <T extends Comparable<T>> T get(Property<T> property) {
T value = getNullable(property);
if (value == null) {
throw new IllegalArgumentException("Property " + property + " not found in " + this.owner.value().id());
}
return value;
}
public <T extends Comparable<T>> T get(Property<T> property, T fallback) {
return Objects.requireNonNullElse(getNullable(property), fallback);
}
@Nullable
public <T extends Comparable<T>> T getNullable(Property<T> property) {
Comparable<?> value = this.propertyMap.get(property);
return value != null ? property.valueClass().cast(value) : null;
}
public CompoundTag propertiesNbt() {
CompoundTag properties = new CompoundTag();
for (Map.Entry<Property<?>, Comparable<?>> entry : this.propertyMap.entrySet()) {
Property<?> property = entry.getKey();
properties.put(property.name(), pack(property, entry.getValue()));
}
return properties;
}
@SuppressWarnings("unchecked")
private static <T extends Comparable<T>> Tag pack(Property<T> property, Object value) {
return property.pack((T) value);
}
@SuppressWarnings("unchecked")
public static <T extends Comparable<T>> ImmutableBlockState with(ImmutableBlockState state, Property<T> property, Object value) {
return state.with(property, (T) value);
}
public ImmutableBlockState with(CompoundTag propertiesNBT) {
CustomBlock owner = this.owner.value();
ImmutableBlockState finalState = this;
for (Map.Entry<String, Tag> entry : propertiesNBT.entrySet()) {
Property<?> property = owner.getProperty(entry.getKey());
if (property != null) {
finalState = with(finalState, property, property.unpack(entry.getValue()));
}
}
return finalState;
}
public <T extends Comparable<T>, V extends T> ImmutableBlockState with(Property<T> property, V value) {
if (!this.propertyMap.containsKey(property)) {
throw new IllegalArgumentException("Property " + property + " not found in " + this.owner.value().id());
}
return withInternal(property, value);
}
private <T extends Comparable<T>, V extends T> ImmutableBlockState withInternal(Property<T> property, V newValue) {
if (newValue.equals(this.propertyMap.get(property))) {
return this;
}
int index = property.indexOf(newValue);
if (index == -1) {
throw new IllegalArgumentException("Invalid value " + newValue + " for property " + property);
}
return this.withMap.get(property)[index];
}
public void createWithMap(Map<Map<Property<?>, Comparable<?>>, ImmutableBlockState> states) {
if (this.withMap != null) {
throw new IllegalStateException("WithMap already initialized");
}
Reference2ObjectArrayMap<Property<?>, ImmutableBlockState[]> map = new Reference2ObjectArrayMap<>(this.propertyMap.size());
for (Property<?> property : this.propertyMap.keySet()) {
ImmutableBlockState[] statesArray = property.possibleValues().stream()
.map(value -> {
Map<Property<?>, Comparable<?>> testMap = new Reference2ObjectArrayMap<>(this.propertyMap);
testMap.put(property, value);
ImmutableBlockState state = states.get(testMap);
if (state == null) {
throw new IllegalStateException("Missing state for " + testMap);
}
return state;
})
.toArray(ImmutableBlockState[]::new);
map.put(property, statesArray);
}
this.withMap = Map.copyOf(map);
}
public Map<Property<?>, Comparable<?>> propertyEntries() {
return Collections.unmodifiableMap(this.propertyMap);
}
}

View File

@@ -1,29 +1,25 @@
package net.momirealms.craftengine.core.block;
import it.unimi.dsi.fastutil.objects.Reference2ObjectArrayMap;
import net.momirealms.craftengine.core.block.behavior.EmptyBlockBehavior;
import net.momirealms.craftengine.core.registry.Holder;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.sparrow.nbt.CompoundTag;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public final class InactiveCustomBlock extends AbstractCustomBlock {
private final Map<CompoundTag, ImmutableBlockState> cachedData = new HashMap<>();
public InactiveCustomBlock(Key id, Holder.Reference<CustomBlock> holder) {
super(id, holder, Map.of(), Map.of(), Map.of(), BlockSettings.of(), Map.of(), List.of(), null);
}
@Override
protected void applyPlatformSettings() {
public InactiveCustomBlock(Holder.Reference<CustomBlock> holder) {
super(holder, new BlockStateVariantProvider(holder, ImmutableBlockState::new, Map.of()), Map.of(), null);
}
@Override
public ImmutableBlockState getBlockState(CompoundTag nbt) {
return this.cachedData.computeIfAbsent(nbt, k -> {
ImmutableBlockState state = new ImmutableBlockState(super.holder, new Reference2ObjectArrayMap<>());
state.setBehavior(EmptyBlockBehavior.INSTANCE);
state.setNbtToSave(state.toNbtToSave(nbt));
return state;
});

View File

@@ -5,5 +5,7 @@ public enum PushReaction {
DESTROY,
BLOCK,
IGNORE,
PUSH_ONLY
PUSH_ONLY;
public static final PushReaction[] VALUES = values();
}

View File

@@ -1,4 +1,6 @@
package net.momirealms.craftengine.core.block.state;
package net.momirealms.craftengine.core.block;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
@@ -11,4 +13,7 @@ public interface StatePropertyAccessor {
boolean hasProperty(String property);
<T> T getPropertyValue(String property);
@NotNull
Object withProperty(String propertyName, String value);
}

View File

@@ -15,7 +15,11 @@ public interface EntityBlockBehavior {
BlockEntity createBlockEntity(BlockPos pos, ImmutableBlockState state);
default <T extends BlockEntity> BlockEntityTicker<T> createBlockEntityTicker(CEWorld level, ImmutableBlockState state, BlockEntityType<T> blockEntityType) {
default <T extends BlockEntity> BlockEntityTicker<T> createSyncBlockEntityTicker(CEWorld level, ImmutableBlockState state, BlockEntityType<T> blockEntityType) {
return null;
}
default <T extends BlockEntity> BlockEntityTicker<T> createAsyncBlockEntityTicker(CEWorld level, ImmutableBlockState state, BlockEntityType<T> blockEntityType) {
return null;
}

View File

@@ -1,5 +1,6 @@
package net.momirealms.craftengine.core.block.parser;
import net.momirealms.craftengine.core.block.BlockStateVariantProvider;
import net.momirealms.craftengine.core.block.CustomBlock;
import net.momirealms.craftengine.core.block.properties.Property;
import net.momirealms.craftengine.core.util.StringReader;
@@ -7,11 +8,13 @@ import net.momirealms.sparrow.nbt.CompoundTag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.Function;
public final class BlockNbtParser {
private BlockNbtParser() {}
@Nullable
public static CompoundTag deserialize(@NotNull CustomBlock block, @NotNull String data) {
public static CompoundTag deserialize(@NotNull Function<String, Property<?>> propertyProvider, @NotNull String data) {
StringReader reader = StringReader.simple(data);
CompoundTag properties = new CompoundTag();
while (reader.canRead()) {
@@ -24,7 +27,7 @@ public final class BlockNbtParser {
if (propertyValue.isEmpty()) {
return null;
}
Property<?> property = block.getProperty(propertyName);
Property<?> property = propertyProvider.apply(propertyName);
if (property != null) {
property.createOptionalTag(propertyValue).ifPresent(tag -> {
properties.put(propertyName, tag);
@@ -38,4 +41,14 @@ public final class BlockNbtParser {
}
return properties;
}
@Nullable
public static CompoundTag deserialize(@NotNull CustomBlock block, @NotNull String data) {
return deserialize(block::getProperty, data);
}
@Nullable
public static CompoundTag deserialize(@NotNull BlockStateVariantProvider variantProvider, @NotNull String data) {
return deserialize(variantProvider::getProperty, data);
}
}

View File

@@ -54,6 +54,16 @@ public class BooleanProperty extends Property<Boolean> {
return bool.toString();
}
@Override
public Boolean valueByName(String name) {
if (name.equals("true")) {
return true;
} else if (name.equals("false")) {
return false;
}
return null;
}
@Override
public int indexOf(Boolean bool) {
return bool ? 0 : 1;

View File

@@ -86,6 +86,11 @@ public class EnumProperty<T extends Enum<T>> extends Property<T> {
return value.name().toLowerCase(Locale.ENGLISH);
}
@Override
public T valueByName(String name) {
return this.names.get(name.toLowerCase(Locale.ENGLISH));
}
@Override
public int indexOf(T value) {
return this.ordinalToIndex[value.ordinal()];

View File

@@ -76,6 +76,16 @@ public class IntegerProperty extends Property<Integer> {
return integer.toString();
}
@Override
public Integer valueByName(String name) {
try {
int i = Integer.parseInt(name);
return i >= this.min && i <= this.max ? i : null;
} catch (NumberFormatException var3) {
return null;
}
}
@Override
public int indexOf(Integer integer) {
return integer <= this.max ? integer - this.min : -1;

View File

@@ -1,6 +1,6 @@
package net.momirealms.craftengine.core.block.properties;
import net.momirealms.craftengine.core.block.state.properties.*;
import net.momirealms.craftengine.core.block.properties.type.*;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.registry.BuiltInRegistries;
import net.momirealms.craftengine.core.registry.Registries;
@@ -22,6 +22,7 @@ public final class Properties {
public static final Key STAIRS_SHAPE = Key.of("craftengine:stairs_shape");
public static final Key SLAB_TYPE = Key.of("craftengine:slab_type");
public static final Key SOFA_SHAPE = Key.of("craftengine:sofa_shape");
public static final Key ANCHOR_TYPE = Key.of("craftengine:anchor_type");
static {
register(BOOLEAN, BooleanProperty.FACTORY);
@@ -38,7 +39,7 @@ public final class Properties {
register(STAIRS_SHAPE, new EnumProperty.Factory<>(StairsShape.class));
register(SLAB_TYPE, new EnumProperty.Factory<>(SlabType.class));
register(SOFA_SHAPE, new EnumProperty.Factory<>(SofaShape.class));
register(ANCHOR_TYPE, new EnumProperty.Factory<>(AnchorType.class));
}
public static void register(Key key, PropertyFactory factory) {

View File

@@ -5,6 +5,7 @@ import net.momirealms.craftengine.core.item.context.BlockPlaceContext;
import net.momirealms.craftengine.core.util.Direction;
import net.momirealms.craftengine.core.util.HorizontalDirection;
import net.momirealms.sparrow.nbt.Tag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
@@ -112,6 +113,9 @@ public abstract class Property<T extends Comparable<T>> {
return indexOf(unpack(tag));
}
@Nullable
public abstract T valueByName(String name);
public abstract int indexOf(T value);
public abstract T unpack(Tag tag);
@@ -152,7 +156,7 @@ public abstract class Property<T extends Comparable<T>> {
}
@Override
public String toString() {
public @NotNull String toString() {
return this.property.name + "=" + this.property.valueName(this.value);
}
}
@@ -167,4 +171,9 @@ public abstract class Property<T extends Comparable<T>> {
public static <T extends Comparable<T>> String formatValue(Property<T> property, Comparable<?> value) {
return property.valueName((T) value);
}
@Override
public String toString() {
return this.getClass().getSimpleName() + "{clazz=" + this.clazz + ", name='" + this.name + "', values=" + this.possibleValues() + '}';
}
}

View File

@@ -6,7 +6,6 @@ import net.momirealms.sparrow.nbt.StringTag;
import net.momirealms.sparrow.nbt.Tag;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
@@ -36,7 +35,7 @@ public class StringProperty extends Property<String> {
@Override
public Optional<String> optional(String valueName) {
return Optional.ofNullable(this.names.get(valueName.toLowerCase(Locale.ENGLISH)));
return Optional.ofNullable(this.names.get(valueName));
}
@Override
@@ -59,7 +58,7 @@ public class StringProperty extends Property<String> {
@Override
public final int idFor(String value) {
int index = indexOf(value.toLowerCase(Locale.ENGLISH));
int index = indexOf(value);
if (index == -1) {
throw new IllegalArgumentException("Invalid value: " + value);
}
@@ -68,12 +67,17 @@ public class StringProperty extends Property<String> {
@Override
public String valueName(String value) {
return value.toLowerCase(Locale.ENGLISH);
return value;
}
@Override
public String valueByName(String name) {
return this.names.get(name);
}
@Override
public int indexOf(String value) {
return values.indexOf(value.toLowerCase(Locale.ENGLISH));
return this.values.indexOf(value);
}
@Override
@@ -92,13 +96,12 @@ public class StringProperty extends Property<String> {
public Property<?> create(String name, Map<String, Object> arguments) {
List<String> values = MiscUtils.getAsStringList(arguments.get("values"))
.stream()
.map(it -> it.toLowerCase(Locale.ENGLISH))
.toList();
String defaultValueName = arguments.getOrDefault("default", "").toString().toLowerCase(Locale.ENGLISH);
String defaultValueName = arguments.getOrDefault("default", "").toString();
String defaultValue = values.stream()
.filter(e -> e.toLowerCase(Locale.ENGLISH).equals(defaultValueName))
.filter(e -> e.equals(defaultValueName))
.findFirst()
.orElseGet(() -> values.get(0));
.orElseGet(values::getFirst);
return StringProperty.create(name, values, defaultValue);
}
}

View File

@@ -0,0 +1,7 @@
package net.momirealms.craftengine.core.block.properties.type;
public enum AnchorType {
FLOOR,
WALL,
CEILING
}

View File

@@ -0,0 +1,5 @@
package net.momirealms.craftengine.core.block.properties.type;
public enum DoorHinge {
LEFT, RIGHT
}

View File

@@ -0,0 +1,5 @@
package net.momirealms.craftengine.core.block.properties.type;
public enum DoubleBlockHalf {
UPPER, LOWER
}

View File

@@ -0,0 +1,5 @@
package net.momirealms.craftengine.core.block.properties.type;
public enum SingleBlockHalf {
TOP, BOTTOM
}

View File

@@ -0,0 +1,7 @@
package net.momirealms.craftengine.core.block.properties.type;
public enum SlabType {
TOP,
BOTTOM,
DOUBLE
}

View File

@@ -1,4 +1,4 @@
package net.momirealms.craftengine.core.block.state.properties;
package net.momirealms.craftengine.core.block.properties.type;
public enum SofaShape {
STRAIGHT,

View File

@@ -1,4 +1,4 @@
package net.momirealms.craftengine.core.block.state.properties;
package net.momirealms.craftengine.core.block.properties.type;
public enum StairsShape {
STRAIGHT,

View File

@@ -1,5 +0,0 @@
package net.momirealms.craftengine.core.block.state.properties;
public enum DoorHinge {
LEFT, RIGHT
}

View File

@@ -1,5 +0,0 @@
package net.momirealms.craftengine.core.block.state.properties;
public enum DoubleBlockHalf {
UPPER, LOWER
}

View File

@@ -1,5 +0,0 @@
package net.momirealms.craftengine.core.block.state.properties;
public enum SingleBlockHalf {
TOP, BOTTOM
}

View File

@@ -1,7 +0,0 @@
package net.momirealms.craftengine.core.block.state.properties;
public enum SlabType {
TOP,
BOTTOM,
DOUBLE
}

View File

@@ -1,5 +1,6 @@
package net.momirealms.craftengine.core.entity;
import net.momirealms.craftengine.core.entity.data.EntityData;
import net.momirealms.craftengine.core.util.Direction;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.world.World;
@@ -30,9 +31,21 @@ public interface Entity {
Direction getDirection();
Object literalObject();
Object platformEntity();
Object serverEntity();
String name();
UUID uuid();
Object entityData();
<T> T getEntityData(EntityData<T> entityData);
default <T> void setEntityData(EntityData<T> data, T value) {
setEntityData(data, value, false);
}
<T> void setEntityData(EntityData<T> data, T value, boolean force);
}

View File

@@ -0,0 +1,35 @@
package net.momirealms.craftengine.core.entity.data;
import java.util.List;
public interface EntityData<T> {
Object serializer();
int id();
T defaultValue();
Object entityDataAccessor();
Object create(Object entityDataAccessor, Object value);
default Object createEntityDataIfNotDefaultValue(T value) {
if (defaultValue().equals(value)) return null;
return create(entityDataAccessor(), value);
}
default Object createEntityData(Object value) {
return create(entityDataAccessor(), value);
}
default void addEntityDataIfNotDefaultValue(T value, List<Object> list) {
if (!defaultValue().equals(value)) {
list.add(create(entityDataAccessor(), value));
}
}
default void addEntityData(T value, List<Object> list) {
list.add(create(entityDataAccessor(), value));
}
}

View File

@@ -5,10 +5,12 @@ import net.momirealms.craftengine.core.entity.ItemDisplayContext;
import net.momirealms.craftengine.core.loot.LootTable;
import net.momirealms.craftengine.core.pack.LoadingSequence;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.pack.PendingConfigSection;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.ConfigParser;
import net.momirealms.craftengine.core.plugin.config.IdSectionConfigParser;
import net.momirealms.craftengine.core.plugin.context.event.EventFunctions;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.util.GsonHelper;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.util.ResourceConfigUtils;
@@ -31,7 +33,7 @@ public abstract class AbstractFurnitureManager implements FurnitureManager {
}
@Override
public ConfigParser parser() {
public FurnitureParser parser() {
return this.furnitureParser;
}
@@ -74,8 +76,26 @@ public abstract class AbstractFurnitureManager implements FurnitureManager {
protected abstract CustomFurniture.Builder furnitureBuilder();
public class FurnitureParser implements ConfigParser {
public class FurnitureParser extends IdSectionConfigParser {
public static final String[] CONFIG_SECTION_NAME = new String[] { "furniture" };
private final List<PendingConfigSection> pendingConfigSections = new ArrayList<>();
public void addPendingConfigSection(PendingConfigSection section) {
this.pendingConfigSections.add(section);
}
@Override
public void preProcess() {
for (PendingConfigSection section : this.pendingConfigSections) {
ResourceConfigUtils.runCatching(
section.path(),
section.node(),
() -> parseSection(section.pack(), section.path(), section.node(), section.id(), section.config()),
() -> GsonHelper.get().toJson(section.config())
);
}
this.pendingConfigSections.clear();
}
@Override
public String[] sectionId() {
@@ -89,8 +109,8 @@ public abstract class AbstractFurnitureManager implements FurnitureManager {
@SuppressWarnings("unchecked")
@Override
public void parseSection(Pack pack, Path path, Key id, Map<String, Object> section) {
if (byId.containsKey(id)) {
public void parseSection(Pack pack, Path path, String node, Key id, Map<String, Object> section) {
if (AbstractFurnitureManager.this.byId.containsKey(id)) {
throw new LocalizedResourceConfigException("warning.config.furniture.duplicate");
}
EnumMap<AnchorType, CustomFurniture.Placement> placements = new EnumMap<>(AnchorType.class);

View File

@@ -6,9 +6,12 @@ import net.momirealms.craftengine.core.entity.AbstractEntity;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.plugin.context.CooldownData;
import net.momirealms.craftengine.core.plugin.network.NetWorkUser;
import net.momirealms.craftengine.core.sound.SoundData;
import net.momirealms.craftengine.core.sound.SoundSource;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.world.BlockPos;
import net.momirealms.craftengine.core.world.Position;
import net.momirealms.craftengine.core.world.Vec3d;
import net.momirealms.craftengine.core.world.WorldPosition;
import org.jetbrains.annotations.NotNull;
@@ -100,7 +103,19 @@ public abstract class Player extends AbstractEntity implements NetWorkUser {
public abstract void playSound(Key sound, SoundSource source, float volume, float pitch);
public abstract void playSound(Key sound, BlockPos pos, SoundSource source, float volume, float pitch);
public abstract void playSound(Position pos, Key sound, SoundSource source, float volume, float pitch);
public void playSound(BlockPos pos, Key sound, SoundSource source, float volume, float pitch) {
this.playSound(Vec3d.atCenterOf(pos), sound, source, volume, pitch);
}
public void playSound(BlockPos pos, SoundData data, SoundSource source) {
this.playSound(pos, data.id(), source, data.volume().get(), data.pitch().get());
}
public void playSound(Position pos, SoundData data, SoundSource source) {
this.playSound(pos, data.id(), source, data.volume().get(), data.pitch().get());
}
public abstract void giveItem(Item<?> item);
@@ -154,4 +169,6 @@ public abstract class Player extends AbstractEntity implements NetWorkUser {
public abstract CooldownData cooldown();
public abstract void teleport(WorldPosition worldPosition);
public abstract void damage(double amount, Key damageType);
}

View File

@@ -3,7 +3,6 @@ package net.momirealms.craftengine.core.entity.projectile;
import net.momirealms.craftengine.core.entity.Billboard;
import net.momirealms.craftengine.core.entity.ItemDisplayContext;
import net.momirealms.craftengine.core.util.Key;
import org.jetbrains.annotations.Nullable;
import org.joml.Quaternionf;
import org.joml.Vector3f;
@@ -13,6 +12,5 @@ public record ProjectileMeta(Key item,
Vector3f scale,
Vector3f translation,
Quaternionf rotation,
double range,
@Nullable ProjectileType type) {
double range) {
}

View File

@@ -1,5 +0,0 @@
package net.momirealms.craftengine.core.entity.projectile;
public enum ProjectileType {
TRIDENT
}

View File

@@ -5,8 +5,11 @@ import net.momirealms.craftengine.core.entity.player.Player;
import net.momirealms.craftengine.core.pack.LoadingSequence;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.pack.ResourceLocation;
import net.momirealms.craftengine.core.pack.allocator.IdAllocator;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.config.ConfigParser;
import net.momirealms.craftengine.core.plugin.config.IdSectionConfigParser;
import net.momirealms.craftengine.core.plugin.context.ContextHolder;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
@@ -23,6 +26,9 @@ import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -53,6 +59,14 @@ public abstract class AbstractFontManager implements FontManager {
this.emojiParser = new EmojiParser();
}
public ImageParser imageParser() {
return imageParser;
}
public EmojiParser emojiParser() {
return emojiParser;
}
@Override
public void load() {
this.offsetFont = Optional.ofNullable(plugin.config().settings().getSection("image.offset-characters"))
@@ -61,6 +75,21 @@ public abstract class AbstractFontManager implements FontManager {
this.networkTagMapper = new HashMap<>(1024);
}
@Override
public OffsetFont offsetFont() {
return offsetFont;
}
@Override
public Map<Key, BitmapImage> loadedImages() {
return Collections.unmodifiableMap(this.images);
}
@Override
public Map<Key, Emoji> emojis() {
return Collections.unmodifiableMap(this.emojis);
}
@Override
public void unload() {
this.fonts.clear();
@@ -213,7 +242,7 @@ public abstract class AbstractFontManager implements FontManager {
emoji.content(),
PlayerOptionalContext.of(player, ContextHolder.builder()
.withOptionalParameter(EmojiParameters.EMOJI, emoji.emojiImage())
.withParameter(EmojiParameters.KEYWORD, emoji.keywords().get(0))
.withParameter(EmojiParameters.KEYWORD, emoji.keywords().getFirst())
).tagResolvers())
);
if (emojis.size() >= maxTimes) break;
@@ -371,7 +400,7 @@ public abstract class AbstractFontManager implements FontManager {
return this.fonts.computeIfAbsent(key, Font::new);
}
public class EmojiParser implements ConfigParser {
public class EmojiParser extends IdSectionConfigParser {
public static final String[] CONFIG_SECTION_NAME = new String[] {"emoji", "emojis"};
@Override
@@ -385,18 +414,18 @@ public abstract class AbstractFontManager implements FontManager {
}
@Override
public void parseSection(Pack pack, Path path, Key id, Map<String, Object> section) {
public void parseSection(Pack pack, Path path, String node, Key id, Map<String, Object> section) {
if (emojis.containsKey(id)) {
throw new LocalizedResourceConfigException("warning.config.emoji.duplicate", path, id);
throw new LocalizedResourceConfigException("warning.config.emoji.duplicate");
}
String permission = (String) section.get("permission");
Object keywordsRaw = section.get("keywords");
if (keywordsRaw == null) {
throw new LocalizedResourceConfigException("warning.config.emoji.missing_keywords", path, id);
throw new LocalizedResourceConfigException("warning.config.emoji.missing_keywords");
}
List<String> keywords = MiscUtils.getAsStringList(keywordsRaw);
if (keywords.isEmpty()) {
throw new LocalizedResourceConfigException("warning.config.emoji.missing_keywords", path, id);
throw new LocalizedResourceConfigException("warning.config.emoji.missing_keywords");
}
Object rawContent = section.getOrDefault("content", "<white><arg:emoji></white>");
String content;
@@ -415,7 +444,7 @@ public abstract class AbstractFontManager implements FontManager {
if (bitmapImage.isPresent()) {
image = bitmapImage.get().miniMessageAt(0, 0);
} else {
throw new LocalizedResourceConfigException("warning.config.emoji.invalid_image", path, id, rawImage);
throw new LocalizedResourceConfigException("warning.config.emoji.invalid_image", rawImage);
}
} else if (split.length == 4) {
Key imageId = new Key(split[0], split[1]);
@@ -424,13 +453,13 @@ public abstract class AbstractFontManager implements FontManager {
try {
image = bitmapImage.get().miniMessageAt(Integer.parseInt(split[2]), Integer.parseInt(split[3]));
} catch (ArrayIndexOutOfBoundsException e) {
throw new LocalizedResourceConfigException("warning.config.emoji.invalid_image", path, id, rawImage);
throw new LocalizedResourceConfigException("warning.config.emoji.invalid_image", rawImage);
}
} else {
throw new LocalizedResourceConfigException("warning.config.emoji.invalid_image", path, id, rawImage);
throw new LocalizedResourceConfigException("warning.config.emoji.invalid_image", rawImage);
}
} else {
throw new LocalizedResourceConfigException("warning.config.emoji.invalid_image", path, id, rawImage);
throw new LocalizedResourceConfigException("warning.config.emoji.invalid_image", rawImage);
}
}
Emoji emoji = new Emoji(content, permission, image, keywords);
@@ -438,8 +467,9 @@ public abstract class AbstractFontManager implements FontManager {
}
}
public class ImageParser implements ConfigParser {
public class ImageParser extends IdSectionConfigParser {
public static final String[] CONFIG_SECTION_NAME = new String[] {"images", "image"};
private final Map<Key, IdAllocator> idAllocators = new HashMap<>();
@Override
public String[] sectionId() {
@@ -452,129 +482,219 @@ public abstract class AbstractFontManager implements FontManager {
}
@Override
public void parseSection(Pack pack, Path path, Key id, Map<String, Object> section) {
if (images.containsKey(id)) {
throw new LocalizedResourceConfigException("warning.config.image.duplicate", path, id);
public void postProcess() {
for (Map.Entry<Key, IdAllocator> entry : this.idAllocators.entrySet()) {
entry.getValue().processPendingAllocations();
try {
entry.getValue().saveToCache();
} catch (IOException e) {
AbstractFontManager.this.plugin.logger().warn("Error while saving codepoint allocation for font " + entry.getKey().asString(), e);
}
}
}
@Override
public void preProcess() {
this.idAllocators.clear();
}
public IdAllocator getOrCreateIdAllocator(Key key) {
return this.idAllocators.computeIfAbsent(key, k -> {
IdAllocator newAllocator = new IdAllocator(plugin.dataFolderPath().resolve("cache").resolve("font").resolve(k.namespace()).resolve(k.value() + ".json"));
newAllocator.reset(Config.codepointStartingValue(k), 1114111); // utf16
try {
newAllocator.loadFromCache();
} catch (IOException e) {
AbstractFontManager.this.plugin.logger().warn("Error while loading chars data from cache for font " + k.asString(), e);
}
return newAllocator;
});
}
@Override
public void parseSection(Pack pack, Path path, String node, Key id, Map<String, Object> section) {
if (AbstractFontManager.this.images.containsKey(id)) {
throw new LocalizedResourceConfigException("warning.config.image.duplicate");
}
Object file = section.get("file");
if (file == null) {
throw new LocalizedResourceConfigException("warning.config.image.missing_file", path, id);
throw new LocalizedResourceConfigException("warning.config.image.missing_file");
}
String resourceLocation = CharacterUtils.replaceBackslashWithSlash(file.toString());
String resourceLocation = MiscUtils.make(CharacterUtils.replaceBackslashWithSlash(file.toString()), s -> s.endsWith(".png") ? s : s + ".png");
if (!ResourceLocation.isValid(resourceLocation)) {
throw new LocalizedResourceConfigException("warning.config.image.invalid_file_chars", path, id, resourceLocation);
throw new LocalizedResourceConfigException("warning.config.image.invalid_file_chars", resourceLocation);
}
String fontName = section.getOrDefault("font", "minecraft:default").toString();
String fontName = section.getOrDefault("font", pack.namespace()+ ":default").toString();
if (!ResourceLocation.isValid(fontName)) {
throw new LocalizedResourceConfigException("warning.config.image.invalid_font_chars", path, id, fontName);
throw new LocalizedResourceConfigException("warning.config.image.invalid_font_chars", fontName);
}
Key fontKey = Key.withDefaultNamespace(fontName, id.namespace());
Font font = getOrCreateFont(fontKey);
List<char[]> chars;
Key fontId = Key.withDefaultNamespace(fontName, id.namespace());
Font font = getOrCreateFont(fontId);
IdAllocator allocator = getOrCreateIdAllocator(fontId);
int rows;
int columns;
List<CompletableFuture<Integer>> futureCodepoints = new ArrayList<>();
Object charsObj = ResourceConfigUtils.get(section, "chars", "char");
// 自动分配
if (charsObj == null) {
throw new LocalizedResourceConfigException("warning.config.image.missing_char", path, id);
}
if (charsObj instanceof List<?> list) {
chars = MiscUtils.getAsStringList(list).stream().map(it -> {
if (it.startsWith("\\u")) {
return CharacterUtils.decodeUnicodeToChars(it);
} else {
return it.toCharArray();
Object grid = section.get("grid-size");
if (grid != null) {
String gridString = grid.toString();
String[] split = gridString.split(",");
if (split.length != 2) {
throw new LocalizedResourceConfigException("warning.config.image.invalid_grid_size", gridString);
}
}).toList();
if (chars.isEmpty()) {
throw new LocalizedResourceConfigException("warning.config.image.missing_char", path, id);
rows = Integer.parseInt(split[0]);
columns = Integer.parseInt(split[1]);
int chars = rows * columns;
if (chars <= 0) {
throw new LocalizedResourceConfigException("warning.config.image.invalid_grid_size", gridString);
}
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
futureCodepoints.add(allocator.requestAutoId(id.asString() + ":" + i + ":" + j));
}
}
} else {
rows = 1;
columns = 1;
futureCodepoints.add(allocator.requestAutoId(id.asString()));
}
} else {
if (charsObj instanceof Integer integer) {
chars = List.of(new char[]{(char) integer.intValue()});
}
// 使用了list
else if (charsObj instanceof List<?> list) {
List<String> charsList = MiscUtils.getAsStringList(list);
if (charsList.isEmpty() || charsList.getFirst().isEmpty()) {
throw new LocalizedResourceConfigException("warning.config.image.missing_char");
}
int tempColumns = -1;
rows = charsList.size();
for (int i = 0; i < charsList.size(); i++) {
String charString = charsList.get(i);
int[] codepoints;
if (charString.startsWith("\\u")) {
codepoints = CharacterUtils.charsToCodePoints(CharacterUtils.decodeUnicodeToChars(charString));
} else {
codepoints = CharacterUtils.charsToCodePoints(charString.toCharArray());
}
for (int j = 0; j < codepoints.length; j++) {
if (codepoints[j] == 0) {
futureCodepoints.add(CompletableFuture.completedFuture(0));
} else {
futureCodepoints.add(allocator.assignFixedId(id.asString() + ":" + i + ":" + j, codepoints[j]));
}
}
if (tempColumns == -1) {
tempColumns = codepoints.length;
} else if (tempColumns != codepoints.length) {
throw new LocalizedResourceConfigException("warning.config.image.invalid_codepoint_grid");
}
}
columns = tempColumns;
}
// 使用了具体的值
else {
if (charsObj instanceof Integer codepoint) {
futureCodepoints.add(allocator.assignFixedId(id.asString(), codepoint));
rows = 1;
columns = 1;
} else {
String character = charsObj.toString();
if (character.isEmpty()) {
throw new LocalizedResourceConfigException("warning.config.image.missing_char", path, id);
throw new LocalizedResourceConfigException("warning.config.image.missing_char");
}
if (character.length() == 1) {
chars = List.of(character.toCharArray());
rows = 1;
int[] codepoints;
if (character.startsWith("\\u")) {
codepoints = CharacterUtils.charsToCodePoints(CharacterUtils.decodeUnicodeToChars(character));
} else {
if (character.startsWith("\\u")) {
chars = List.of(CharacterUtils.decodeUnicodeToChars(character));
codepoints = CharacterUtils.charsToCodePoints(character.toCharArray());
}
columns = codepoints.length;
for (int i = 0; i < codepoints.length; i++) {
if (codepoints[i] == 0) {
futureCodepoints.add(CompletableFuture.completedFuture(0));
} else {
// ??? TODO 需要测试特殊字符集
// if (CharacterUtils.containsCombinedCharacter(character)) {
// TranslationManager.instance().log("warning.config.image.invalid_char", path.toString(), id.toString());
// }
chars = List.of(character.toCharArray());
futureCodepoints.add(allocator.assignFixedId(id.asString() + ":0:" + i, codepoints[i]));
}
}
}
}
int size = -1;
int[][] codepointGrid = new int[chars.size()][];
for (int i = 0; i < chars.size(); ++i) {
int[] codepoints = CharacterUtils.charsToCodePoints(chars.get(i));
for (int codepoint : codepoints) {
if (font.isCodepointInUse(codepoint)) {
BitmapImage image = font.bitmapImageByCodepoint(codepoint);
throw new LocalizedResourceConfigException("warning.config.image.codepoint_conflict", path, id,
fontKey.toString(),
CharacterUtils.encodeCharsToUnicode(Character.toChars(codepoint)),
new String(Character.toChars(codepoint)),
image.id().toString());
CompletableFutures.allOf(futureCodepoints).whenComplete((v, t) -> ResourceConfigUtils.runCatching(path, node, () -> {
if (t != null) {
if (t instanceof CompletionException e) {
Throwable cause = e.getCause();
if (cause instanceof IdAllocator.IdConflictException conflict) {
throw new LocalizedResourceConfigException("warning.config.image.codepoint.conflict",
fontId.toString(),
CharacterUtils.encodeCharsToUnicode(Character.toChars(conflict.id())),
new String(Character.toChars(conflict.id())),
conflict.previousOwner()
);
} else if (cause instanceof IdAllocator.IdExhaustedException) {
throw new LocalizedResourceConfigException("warning.config.image.codepoint.exhausted", fontId.asString());
}
}
throw new RuntimeException("Unknown error occurred", t);
}
int[][] codepointGrid = new int[rows][columns];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
try {
int codepoint = futureCodepoints.get(i * columns + j).get();
codepointGrid[i][j] = codepoint;
} catch (InterruptedException | ExecutionException e) {
AbstractFontManager.this.plugin.logger().warn("Interrupted while allocating codepoint for image " + id.asString(), e);
return;
}
}
}
if (codepoints.length == 0) {
throw new LocalizedResourceConfigException("warning.config.image.missing_char", path, id);
}
codepointGrid[i] = codepoints;
if (size == -1) size = codepoints.length;
if (size != codepoints.length) {
throw new LocalizedResourceConfigException("warning.config.image.invalid_codepoint_grid", path, id);
}
}
Object heightObj = section.get("height");
if (!resourceLocation.endsWith(".png")) resourceLocation += ".png";
if (heightObj == null) {
Key namespacedPath = Key.of(resourceLocation);
Path targetImagePath = pack.resourcePackFolder()
.resolve("assets")
.resolve(namespacedPath.namespace())
.resolve("textures")
.resolve(namespacedPath.value());
if (Files.exists(targetImagePath)) {
try (InputStream in = Files.newInputStream(targetImagePath)) {
BufferedImage image = ImageIO.read(in);
heightObj = image.getHeight() / codepointGrid.length;
} catch (IOException e) {
plugin.logger().warn("Failed to load image " + targetImagePath, e);
return;
Object heightObj = section.get("height");
if (heightObj == null) {
Key namespacedPath = Key.of(resourceLocation);
Path targetImagePath = pack.resourcePackFolder()
.resolve("assets")
.resolve(namespacedPath.namespace())
.resolve("textures")
.resolve(namespacedPath.value());
if (Files.exists(targetImagePath)) {
try (InputStream in = Files.newInputStream(targetImagePath)) {
BufferedImage image = ImageIO.read(in);
heightObj = image.getHeight() / codepointGrid.length;
} catch (IOException e) {
plugin.logger().warn("Failed to load image " + targetImagePath, e);
return;
}
} else {
throw new LocalizedResourceConfigException("warning.config.image.missing_height");
}
} else {
throw new LocalizedResourceConfigException("warning.config.image.missing_height", path, id);
}
}
int height = ResourceConfigUtils.getAsInt(heightObj, "height");
int ascent = ResourceConfigUtils.getAsInt(section.getOrDefault("ascent", height - 1), "ascent");
if (height < ascent) {
throw new LocalizedResourceConfigException("warning.config.image.height_ascent_conflict", path, id, String.valueOf(height), String.valueOf(ascent));
}
BitmapImage bitmapImage = new BitmapImage(id, fontKey, height, ascent, resourceLocation, codepointGrid);
for (int[] y : codepointGrid) {
for (int x : y) {
font.addBitmapImage(x, bitmapImage);
int height = ResourceConfigUtils.getAsInt(heightObj, "height");
int ascent = ResourceConfigUtils.getAsInt(section.getOrDefault("ascent", height - 1), "ascent");
if (height < ascent) {
throw new LocalizedResourceConfigException("warning.config.image.height_ascent_conflict", String.valueOf(height), String.valueOf(ascent));
}
}
images.put(id, bitmapImage);
BitmapImage bitmapImage = new BitmapImage(id, fontId, height, ascent, resourceLocation, codepointGrid);
for (int[] y : codepointGrid) {
for (int x : y) {
font.addBitmapImage(x, bitmapImage);
}
}
AbstractFontManager.this.images.put(id, bitmapImage);
}, () -> GsonHelper.get().toJson(section)));
}
}
}

View File

@@ -46,6 +46,12 @@ public interface FontManager extends Manageable {
IllegalCharacterProcessResult processIllegalCharacters(String raw, char replacement);
OffsetFont offsetFont();
Map<Key, BitmapImage> loadedImages();
Map<Key, Emoji> emojis();
ConfigParser[] parsers();
default EmojiTextProcessResult replaceMiniMessageEmoji(@NotNull String miniMessage, @Nullable Player player) {

View File

@@ -12,7 +12,8 @@ import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
public class OffsetFont {
private final String font;
private static OffsetFont instance;
private final net.momirealms.craftengine.core.util.Key font;
private final Key fontKey;
private final String NEG_16;
@@ -39,10 +40,14 @@ public class OffsetFont {
.maximumSize(256)
.build();
public OffsetFont(Section section) {
font = section.getString("font", "minecraft:default");
fontKey = Key.key(font);
public net.momirealms.craftengine.core.util.Key font() {
return font;
}
@SuppressWarnings("all")
public OffsetFont(Section section) {
font = net.momirealms.craftengine.core.util.Key.of(section.getString("font", "minecraft:default"));
fontKey = Key.key(font.namespace(), font.value());
NEG_16 = convertIfUnicode(section.getString("-16", ""));
NEG_24 = convertIfUnicode(section.getString("-24", ""));
NEG_32 = convertIfUnicode(section.getString("-32", ""));
@@ -71,7 +76,7 @@ public class OffsetFont {
public String createOffset(int offset, BiFunction<String, String, String> tagDecorator) {
if (offset == 0) return "";
return tagDecorator.apply(this.fastLookup.get(offset, k -> k > 0 ? createPos(k) : createNeg(-k)), this.font);
return tagDecorator.apply(this.fastLookup.get(offset, k -> k > 0 ? createPos(k) : createNeg(-k)), this.font.asString());
}
@SuppressWarnings("DuplicatedCode")
@@ -148,7 +153,7 @@ public class OffsetFont {
private String convertIfUnicode(String s) {
if (s.startsWith("\\u")) {
return new String(CharacterUtils.decodeUnicodeToChars(font));
return new String(CharacterUtils.decodeUnicodeToChars(font.asString()));
} else {
return s;
}

View File

@@ -14,6 +14,7 @@ import net.momirealms.craftengine.core.pack.AbstractPackManager;
import net.momirealms.craftengine.core.pack.LoadingSequence;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.pack.ResourceLocation;
import net.momirealms.craftengine.core.pack.allocator.IdAllocator;
import net.momirealms.craftengine.core.pack.model.*;
import net.momirealms.craftengine.core.pack.model.generation.AbstractModelGenerator;
import net.momirealms.craftengine.core.pack.model.generation.ModelGeneration;
@@ -22,17 +23,21 @@ import net.momirealms.craftengine.core.pack.model.select.TrimMaterialSelectPrope
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.config.ConfigParser;
import net.momirealms.craftengine.core.plugin.config.IdSectionConfigParser;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.context.event.EventFunctions;
import net.momirealms.craftengine.core.plugin.context.event.EventTrigger;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.plugin.logger.Debugger;
import net.momirealms.craftengine.core.registry.BuiltInRegistries;
import net.momirealms.craftengine.core.util.*;
import org.incendo.cloud.suggestion.Suggestion;
import org.incendo.cloud.type.Either;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Stream;
@@ -48,14 +53,15 @@ public abstract class AbstractItemManager<I> extends AbstractModelGenerator impl
protected final Map<Key, CustomItem<I>> customItemsById = new HashMap<>();
protected final Map<String, CustomItem<I>> customItemsByPath = new HashMap<>();
protected final Map<Key, List<UniqueKey>> customItemTags = new HashMap<>();
protected final Map<Key, Map<Integer, Key>> cmdConflictChecker = new HashMap<>();
protected final Map<Key, ModernItemModel> modernItemModels1_21_4 = new HashMap<>();
protected final Map<Key, TreeSet<LegacyOverridesModel>> modernItemModels1_21_2 = new HashMap<>();
protected final Map<Key, TreeSet<LegacyOverridesModel>> legacyOverrides = new HashMap<>();
protected final Map<Key, TreeMap<Integer, ModernItemModel>> modernOverrides = new HashMap<>();
protected final Map<Key, Equipment> equipments = new HashMap<>();
// Cached command suggestions
protected final List<Suggestion> cachedSuggestions = new ArrayList<>();
protected final List<Suggestion> cachedCustomItemSuggestions = new ArrayList<>();
protected final List<Suggestion> cachedAllItemSuggestions = new ArrayList<>();
protected final List<Suggestion> cachedVanillaItemSuggestions = new ArrayList<>();
protected final List<Suggestion> cachedTotemSuggestions = new ArrayList<>();
protected AbstractItemManager(CraftEngine plugin) {
@@ -65,6 +71,14 @@ public abstract class AbstractItemManager<I> extends AbstractModelGenerator impl
ItemDataModifiers.init();
}
public ItemParser itemParser() {
return itemParser;
}
public EquipmentParser equipmentParser() {
return equipmentParser;
}
protected static void registerVanillaItemExtraBehavior(ItemBehavior behavior, Key... items) {
for (Key key : items) {
VANILLA_ITEM_EXTRA_BEHAVIORS.computeIfAbsent(key, k -> new ArrayList<>()).add(behavior);
@@ -118,13 +132,13 @@ public abstract class AbstractItemManager<I> extends AbstractModelGenerator impl
super.clearModelsToGenerate();
this.customItemsById.clear();
this.customItemsByPath.clear();
this.cachedSuggestions.clear();
this.cachedCustomItemSuggestions.clear();
this.cachedAllItemSuggestions.clear();
this.cachedTotemSuggestions.clear();
this.legacyOverrides.clear();
this.modernOverrides.clear();
this.customItemTags.clear();
this.equipments.clear();
this.cmdConflictChecker.clear();
this.modernItemModels1_21_4.clear();
this.modernItemModels1_21_2.clear();
}
@@ -170,7 +184,7 @@ public abstract class AbstractItemManager<I> extends AbstractModelGenerator impl
this.customItemsByPath.put(id.value(), customItem);
if (!customItem.isVanillaItem()) {
// cache command suggestions
this.cachedSuggestions.add(Suggestion.suggestion(id.toString()));
this.cachedCustomItemSuggestions.add(Suggestion.suggestion(id.toString()));
// totem animations
if (VersionHelper.isOrAbove1_21_2()) {
this.cachedTotemSuggestions.add(Suggestion.suggestion(id.toString()));
@@ -197,8 +211,13 @@ public abstract class AbstractItemManager<I> extends AbstractModelGenerator impl
}
@Override
public Collection<Suggestion> cachedSuggestions() {
return Collections.unmodifiableCollection(this.cachedSuggestions);
public Collection<Suggestion> cachedCustomItemSuggestions() {
return Collections.unmodifiableCollection(this.cachedCustomItemSuggestions);
}
@Override
public Collection<Suggestion> cachedAllItemSuggestions() {
return Collections.unmodifiableCollection(this.cachedAllItemSuggestions);
}
@Override
@@ -228,6 +247,12 @@ public abstract class AbstractItemManager<I> extends AbstractModelGenerator impl
}
}
@Override
public void delayedLoad() {
this.cachedAllItemSuggestions.addAll(this.cachedVanillaItemSuggestions);
this.cachedAllItemSuggestions.addAll(this.cachedCustomItemSuggestions);
}
@Override
public Map<Key, CustomItem<I>> loadedItems() {
return Collections.unmodifiableMap(this.customItemsById);
@@ -267,7 +292,7 @@ public abstract class AbstractItemManager<I> extends AbstractModelGenerator impl
protected abstract void registerArmorTrimPattern(Collection<Key> equipments);
public class EquipmentParser implements ConfigParser {
public class EquipmentParser extends IdSectionConfigParser {
public static final String[] CONFIG_SECTION_NAME = new String[] {"equipments", "equipment"};
@Override
@@ -281,7 +306,7 @@ public abstract class AbstractItemManager<I> extends AbstractModelGenerator impl
}
@Override
public void parseSection(Pack pack, Path path, Key id, Map<String, Object> section) {
public void parseSection(Pack pack, Path path, String node, Key id, Map<String, Object> section) {
if (AbstractItemManager.this.equipments.containsKey(id)) {
throw new LocalizedResourceConfigException("warning.config.equipment.duplicate");
}
@@ -310,8 +335,9 @@ public abstract class AbstractItemManager<I> extends AbstractModelGenerator impl
}
}
public class ItemParser implements ConfigParser {
public class ItemParser extends IdSectionConfigParser {
public static final String[] CONFIG_SECTION_NAME = new String[] {"items", "item"};
private final Map<Key, IdAllocator> idAllocators = new HashMap<>();
private boolean isModernFormatRequired() {
return Config.packMaxVersion().isAtOrAbove(MinecraftVersions.V1_21_4);
@@ -321,6 +347,18 @@ public abstract class AbstractItemManager<I> extends AbstractModelGenerator impl
return Config.packMinVersion().isBelow(MinecraftVersions.V1_21_4);
}
private boolean needsCustomModelDataCompatibility() {
return Config.packMinVersion().isBelow(MinecraftVersions.V1_21_2);
}
private boolean needsItemModelCompatibility() {
return Config.packMaxVersion().isAtOrAbove(MinecraftVersions.V1_21_2) && VersionHelper.isOrAbove1_21_2(); //todo 能否通过客户端包解决问题
}
public Map<Key, IdAllocator> idAllocators() {
return this.idAllocators;
}
@Override
public String[] sectionId() {
return CONFIG_SECTION_NAME;
@@ -332,218 +370,311 @@ public abstract class AbstractItemManager<I> extends AbstractModelGenerator impl
}
@Override
public void parseSection(Pack pack, Path path, Key id, Map<String, Object> section) {
public void preProcess() {
this.idAllocators.clear();
}
@Override
public void postProcess() {
for (Map.Entry<Key, IdAllocator> entry : this.idAllocators.entrySet()) {
entry.getValue().processPendingAllocations();
try {
entry.getValue().saveToCache();
} catch (IOException e) {
AbstractItemManager.this.plugin.logger().warn("Error while saving custom model data allocation for material " + entry.getKey().asString(), e);
}
}
}
// 创建或获取已有的自动分配器
private IdAllocator getOrCreateIdAllocator(Key key) {
return this.idAllocators.computeIfAbsent(key, k -> {
IdAllocator newAllocator = new IdAllocator(plugin.dataFolderPath().resolve("cache").resolve("custom-model-data").resolve(k.value() + ".json"));
newAllocator.reset(Config.customModelDataStartingValue(k), 16_777_216);
try {
newAllocator.loadFromCache();
} catch (IOException e) {
AbstractItemManager.this.plugin.logger().warn("Error while loading custom model data from cache for material " + k.asString(), e);
}
return newAllocator;
});
}
@Override
public void parseSection(Pack pack, Path path, String node, Key id, Map<String, Object> section) {
if (AbstractItemManager.this.customItemsById.containsKey(id)) {
throw new LocalizedResourceConfigException("warning.config.item.duplicate");
}
// 创建UniqueKey仅缓存用
UniqueKey uniqueId = UniqueKey.create(id);
// 判断是不是原版物品
boolean isVanillaItem = isVanillaItem(id);
Key material = Key.from(isVanillaItem ? id.value() : ResourceConfigUtils.requireNonEmptyStringOrThrow(section.get("material"), "warning.config.item.missing_material").toLowerCase(Locale.ENGLISH));
Key clientBoundMaterial = section.containsKey("client-bound-material") ? Key.from(section.get("client-bound-material").toString().toLowerCase(Locale.ENGLISH)) : material;
// 如果是原版物品那么custom-model-data只能是0即使用户设置了其他值
int customModelData = isVanillaItem ? 0 : ResourceConfigUtils.getAsInt(section.getOrDefault("custom-model-data", 0), "custom-model-data");
boolean clientBoundModel = section.containsKey("client-bound-model") ? ResourceConfigUtils.getAsBoolean(section.get("client-bound-model"), "client-bound-model") : Config.globalClientboundModel();
if (customModelData < 0) {
throw new LocalizedResourceConfigException("warning.config.item.invalid_custom_model_data", String.valueOf(customModelData));
}
if (customModelData > 16_777_216) {
throw new LocalizedResourceConfigException("warning.config.item.bad_custom_model_data", String.valueOf(customModelData));
}
// item-model值
Key itemModelKey = null;
// 读取服务端侧材质
Key material = isVanillaItem ? id : Key.from(ResourceConfigUtils.requireNonEmptyStringOrThrow(section.get("material"), "warning.config.item.missing_material").toLowerCase(Locale.ROOT));
// 读取客户端侧材质
Key clientBoundMaterial = VersionHelper.PREMIUM && section.containsKey("client-bound-material") ? Key.from(section.get("client-bound-material").toString().toLowerCase(Locale.ROOT)) : material;
CustomItem.Builder<I> itemBuilder = createPlatformItemBuilder(uniqueId, material, clientBoundMaterial);
boolean hasItemModelSection = section.containsKey("item-model");
// custom model data
CompletableFuture<Integer> customModelDataFuture;
boolean forceCustomModelData;
// 如果custom-model-data不为0
if (customModelData > 0) {
if (clientBoundModel) itemBuilder.clientBoundDataModifier(new CustomModelDataModifier<>(customModelData));
else itemBuilder.dataModifier(new CustomModelDataModifier<>(customModelData));
}
// 如果没有item-model选项被配置同时这个物品又含有 model 区域
else if (!hasItemModelSection && section.containsKey("model") && VersionHelper.isOrAbove1_21_2()) {
// 那么使用物品id当成item-model的值
itemModelKey = Key.from(section.getOrDefault("item-model", id.toString()).toString());
// 但是有个前提id必须是有效的resource location
if (ResourceLocation.isValid(itemModelKey.toString())) {
if (clientBoundModel) itemBuilder.clientBoundDataModifier(new ItemModelModifier<>(itemModelKey));
else itemBuilder.dataModifier(new ItemModelModifier<>(itemModelKey));
} else {
itemModelKey = null;
if (!isVanillaItem) {
// 如果用户指定了,说明要手动分配,不管他是什么版本,都强制设置模型值
if (section.containsKey("custom-model-data")) {
int customModelData = ResourceConfigUtils.getAsInt(section.getOrDefault("custom-model-data", 0), "custom-model-data");
if (customModelData < 0) {
throw new LocalizedResourceConfigException("warning.config.item.invalid_custom_model_data", String.valueOf(customModelData));
}
if (customModelData > 16_777_216) {
throw new LocalizedResourceConfigException("warning.config.item.bad_custom_model_data", String.valueOf(customModelData));
}
customModelDataFuture = getOrCreateIdAllocator(clientBoundMaterial).assignFixedId(id.asString(), customModelData);
forceCustomModelData = true;
}
}
// 如果有item-model
if (hasItemModelSection && VersionHelper.isOrAbove1_21_2()) {
itemModelKey = Key.from(section.get("item-model").toString());
if (clientBoundModel) itemBuilder.clientBoundDataModifier(new ItemModelModifier<>(itemModelKey));
else itemBuilder.dataModifier(new ItemModelModifier<>(itemModelKey));
}
// 对于不重要的配置,可以仅警告,不返回
ExceptionCollector<LocalizedResourceConfigException> collector = new ExceptionCollector<>();
// 应用物品数据
try {
applyDataModifiers(MiscUtils.castToMap(section.get("data"), true), itemBuilder::dataModifier);
} catch (LocalizedResourceConfigException e) {
collector.add(e);
}
// 应用客户端侧数据
try {
if (VersionHelper.PREMIUM) {
applyDataModifiers(MiscUtils.castToMap(section.get("client-bound-data"), true), itemBuilder::clientBoundDataModifier);
}
} catch (LocalizedResourceConfigException e) {
collector.add(e);
}
// 如果不是原版物品那么加入ce的标识符
if (!isVanillaItem)
itemBuilder.dataModifier(new IdModifier<>(id));
// 事件
Map<EventTrigger, List<net.momirealms.craftengine.core.plugin.context.function.Function<PlayerOptionalContext>>> eventTriggerListMap;
try {
eventTriggerListMap = EventFunctions.parseEvents(ResourceConfigUtils.get(section, "events", "event"));
} catch (LocalizedResourceConfigException e) {
collector.add(e);
eventTriggerListMap = Map.of();
}
// 设置
ItemSettings settings;
try {
settings = Optional.ofNullable(ResourceConfigUtils.get(section, "settings"))
.map(map -> ItemSettings.fromMap(MiscUtils.castToMap(map, true)))
.map(it -> isVanillaItem ? it.canPlaceRelatedVanillaBlock(true) : it)
.orElse(ItemSettings.of().canPlaceRelatedVanillaBlock(isVanillaItem));
} catch (LocalizedResourceConfigException e) {
collector.add(e);
settings = ItemSettings.of().canPlaceRelatedVanillaBlock(isVanillaItem);
}
// 行为
List<ItemBehavior> behaviors;
try {
behaviors = ItemBehaviors.fromObj(pack, path, id, ResourceConfigUtils.get(section, "behavior", "behaviors"));
} catch (LocalizedResourceConfigException e) {
collector.add(e);
behaviors = Collections.emptyList();
}
// 如果有物品更新器
if (section.containsKey("updater")) {
Map<String, Object> updater = ResourceConfigUtils.getAsMap(section.get("updater"), "updater");
List<ItemUpdateConfig.Version> versions = new ArrayList<>(2);
for (Map.Entry<String, Object> entry : updater.entrySet()) {
try {
int version = Integer.parseInt(entry.getKey());
versions.add(new ItemUpdateConfig.Version(
version,
ResourceConfigUtils.parseConfigAsList(entry.getValue(), map -> ItemUpdaters.fromMap(id, map)).toArray(new ItemUpdater[0])
));
} catch (NumberFormatException ignored) {
// 用户没指定custom-model-data则看当前资源包版本兼容需求
else {
forceCustomModelData = false;
// 如果最低版本要1.21.1以下支持
if (needsCustomModelDataCompatibility()) {
customModelDataFuture = getOrCreateIdAllocator(clientBoundMaterial).requestAutoId(id.asString());
}
// 否则不主动分配模型值
else {
customModelDataFuture = CompletableFuture.completedFuture(0);
}
}
ItemUpdateConfig config = new ItemUpdateConfig(versions);
itemBuilder.updater(config);
itemBuilder.dataModifier(new ItemVersionModifier<>(config.maxVersion()));
} else {
forceCustomModelData = false;
// 原版物品不应该有这个
customModelDataFuture = CompletableFuture.completedFuture(0);
}
// 构建自定义物品
CustomItem<I> customItem = itemBuilder
.isVanillaItem(isVanillaItem)
.behaviors(behaviors)
.settings(settings)
.events(eventTriggerListMap)
.build();
// 添加到缓存
addCustomItem(customItem);
// 如果有类别,则添加
if (section.containsKey("category")) {
AbstractItemManager.this.plugin.itemBrowserManager().addExternalCategoryMember(id, MiscUtils.getAsStringList(section.get("category")).stream().map(Key::of).toList());
}
// 模型配置区域如果这里被配置了那么用户必须要配置custom-model-data或item-model
Map<String, Object> modelSection = MiscUtils.castToMap(section.get("model"), true);
Map<String, Object> legacyModelSection = MiscUtils.castToMap(section.get("legacy-model"), true);
if (modelSection == null && legacyModelSection == null) {
collector.throwIfPresent();
return;
}
boolean needsModelSection = isModernFormatRequired() || (needsLegacyCompatibility() && legacyModelSection == null);
// 只对自定义物品有这个限制
if (!isVanillaItem) {
// 既没有模型值也没有item-model
if (customModelData == 0 && itemModelKey == null) {
collector.addAndThrow(new LocalizedResourceConfigException("warning.config.item.missing_model_id"));
// 当模型值完成分配的时候
customModelDataFuture.whenComplete((cmd, throwable) -> ResourceConfigUtils.runCatching(path, node, () -> {
int customModelData;
if (throwable != null) {
// 检测custom model data 冲突
if (throwable instanceof IdAllocator.IdConflictException exception) {
if (section.containsKey("model") || section.containsKey("legacy-model")) {
throw new LocalizedResourceConfigException("warning.config.item.custom_model_data.conflict", String.valueOf(exception.id()), exception.previousOwner());
}
customModelData = exception.id();
}
// custom model data 已被用尽,不太可能
else if (throwable instanceof IdAllocator.IdExhaustedException) {
throw new LocalizedResourceConfigException("warning.config.item.custom_model_data.exhausted", clientBoundMaterial.asString());
}
// 未知错误
else {
Debugger.ITEM.warn(() -> "Unknown error while allocating custom model data.", throwable);
return;
}
} else {
customModelData = cmd;
}
}
// 新版格式
ItemModel modernModel = null;
// 旧版格式
TreeSet<LegacyOverridesModel> legacyOverridesModels = null;
// 如果需要支持新版item model 或者用户需要旧版本兼容但是没配置legacy-model
if (needsModelSection) {
// 1.21.4+必须要配置model区域如果不需要高版本兼容则可以只写legacy-model
if (modelSection == null) {
collector.addAndThrow(new LocalizedResourceConfigException("warning.config.item.missing_model"));
return;
// item model
Key itemModel = null;
boolean forceItemModel = false;
// 如果这个版本可以使用 item model
if (!isVanillaItem && needsItemModelCompatibility()) {
// 如果用户主动设定了item model那么肯定要设置
if (section.containsKey("item-model")) {
itemModel = Key.from(section.get("item-model").toString());
forceItemModel = true;
}
// 用户没设置item model也没设置custom model data那么为他生成一个基于物品id的item model
else if (customModelData == 0 || Config.alwaysUseItemModel()) {
itemModel = id;
}
// 用户没设置item model但是有custom model data那么就使用custom model data
}
// 是否使用客户端侧模型
boolean clientBoundModel = VersionHelper.PREMIUM && (section.containsKey("client-bound-model") ? ResourceConfigUtils.getAsBoolean(section.get("client-bound-model"), "client-bound-model") : Config.globalClientboundModel());
CustomItem.Builder<I> itemBuilder = createPlatformItemBuilder(uniqueId, material, clientBoundMaterial);
// 模型配置区域如果这里被配置了那么用户必须要配置custom-model-data或item-model
Map<String, Object> modelSection = MiscUtils.castToMap(section.get("model"), true);
Map<String, Object> legacyModelSection = MiscUtils.castToMap(section.get("legacy-model"), true);
boolean hasModelSection = modelSection != null || legacyModelSection != null;
if (customModelData > 0 && (hasModelSection || forceCustomModelData)) {
if (clientBoundModel) itemBuilder.clientBoundDataModifier(new CustomModelDataModifier<>(customModelData));
else itemBuilder.dataModifier(new CustomModelDataModifier<>(customModelData));
}
if (itemModel != null && (hasModelSection || forceItemModel)) {
if (clientBoundModel) itemBuilder.clientBoundDataModifier(new ItemModelModifier<>(itemModel));
else itemBuilder.dataModifier(new ItemModelModifier<>(itemModel));
}
// 对于不重要的配置,可以仅警告,不返回
ExceptionCollector<LocalizedResourceConfigException> collector = new ExceptionCollector<>();
// 应用物品数据
try {
modernModel = ItemModels.fromMap(modelSection);
for (ModelGeneration generation : modernModel.modelsToGenerate()) {
prepareModelGeneration(generation);
applyDataModifiers(MiscUtils.castToMap(section.get("data"), true), itemBuilder::dataModifier);
} catch (LocalizedResourceConfigException e) {
collector.add(e);
}
// 应用客户端侧数据
try {
if (VersionHelper.PREMIUM) {
applyDataModifiers(MiscUtils.castToMap(section.get("client-bound-data"), true), itemBuilder::clientBoundDataModifier);
}
} catch (LocalizedResourceConfigException e) {
collector.addAndThrow(e);
collector.add(e);
}
}
// 如果需要旧版本兼容
if (needsLegacyCompatibility()) {
if (legacyModelSection != null) {
// 如果不是原版物品那么加入ce的标识符
if (!isVanillaItem)
itemBuilder.dataModifier(new IdModifier<>(id));
// 事件
Map<EventTrigger, List<net.momirealms.craftengine.core.plugin.context.function.Function<PlayerOptionalContext>>> eventTriggerListMap;
try {
eventTriggerListMap = EventFunctions.parseEvents(ResourceConfigUtils.get(section, "events", "event"));
} catch (LocalizedResourceConfigException e) {
collector.add(e);
eventTriggerListMap = Map.of();
}
// 设置
ItemSettings settings;
try {
settings = Optional.ofNullable(ResourceConfigUtils.get(section, "settings"))
.map(map -> ItemSettings.fromMap(MiscUtils.castToMap(map, true)))
.map(it -> isVanillaItem ? it.canPlaceRelatedVanillaBlock(true) : it)
.orElse(ItemSettings.of().canPlaceRelatedVanillaBlock(isVanillaItem));
} catch (LocalizedResourceConfigException e) {
collector.add(e);
settings = ItemSettings.of().canPlaceRelatedVanillaBlock(isVanillaItem);
}
// 行为
List<ItemBehavior> behaviors;
try {
behaviors = ResourceConfigUtils.parseConfigAsList(ResourceConfigUtils.get(section, "behavior", "behaviors"), map -> ItemBehaviors.fromMap(pack, path, node, id, map));
} catch (LocalizedResourceConfigException e) {
collector.add(e);
behaviors = Collections.emptyList();
}
// 如果有物品更新器
if (section.containsKey("updater")) {
Map<String, Object> updater = ResourceConfigUtils.getAsMap(section.get("updater"), "updater");
List<ItemUpdateConfig.Version> versions = new ArrayList<>(2);
for (Map.Entry<String, Object> entry : updater.entrySet()) {
try {
int version = Integer.parseInt(entry.getKey());
versions.add(new ItemUpdateConfig.Version(
version,
ResourceConfigUtils.parseConfigAsList(entry.getValue(), map -> ItemUpdaters.fromMap(id, map)).toArray(new ItemUpdater[0])
));
} catch (NumberFormatException ignored) {
}
}
ItemUpdateConfig config = new ItemUpdateConfig(versions);
itemBuilder.updater(config);
itemBuilder.dataModifier(new ItemVersionModifier<>(config.maxVersion()));
}
// 构建自定义物品
CustomItem<I> customItem = itemBuilder
.isVanillaItem(isVanillaItem)
.behaviors(behaviors)
.settings(settings)
.events(eventTriggerListMap)
.build();
// 添加到缓存
addCustomItem(customItem);
// 如果有类别,则添加
if (section.containsKey("category")) {
AbstractItemManager.this.plugin.itemBrowserManager().addExternalCategoryMember(id, MiscUtils.getAsStringList(section.get("category")).stream().map(Key::of).toList());
}
/*
* ========================
*
* 模型配置分界线
*
* ========================
*/
// 原版物品还改模型自己替换json去
if (isVanillaItem) {
return;
}
if (!hasModelSection) {
collector.throwIfPresent();
return;
}
// 只对自定义物品有这个限制既没有模型值也没有item-model
if (customModelData == 0 && itemModel == null) {
collector.addAndThrow(new LocalizedResourceConfigException("warning.config.item.missing_model_id"));
}
// 新版格式
ItemModel modernModel = null;
// 旧版格式
TreeSet<LegacyOverridesModel> legacyOverridesModels = null;
// 如果需要支持新版item model 或者用户需要旧版本兼容但是没配置legacy-model
if (isModernFormatRequired() || (needsLegacyCompatibility() && legacyModelSection == null)) {
// 1.21.4+必须要配置model区域如果不需要高版本兼容则可以只写legacy-model
if (modelSection == null) {
collector.addAndThrow(new LocalizedResourceConfigException("warning.config.item.missing_model"));
return;
}
try {
LegacyItemModel legacyItemModel = LegacyItemModel.fromMap(legacyModelSection, customModelData);
for (ModelGeneration generation : legacyItemModel.modelsToGenerate()) {
modernModel = ItemModels.fromMap(modelSection);
for (ModelGeneration generation : modernModel.modelsToGenerate()) {
prepareModelGeneration(generation);
}
legacyOverridesModels = new TreeSet<>(legacyItemModel.overrides());
} catch (LocalizedResourceConfigException e) {
collector.addAndThrow(e);
}
} else {
legacyOverridesModels = new TreeSet<>();
processModelRecursively(modernModel, new LinkedHashMap<>(), legacyOverridesModels, clientBoundMaterial, customModelData);
if (legacyOverridesModels.isEmpty()) {
collector.add(new LocalizedResourceConfigException("warning.config.item.legacy_model.cannot_convert", path.toString(), id.asString()));
}
// 如果需要旧版本兼容
if (needsLegacyCompatibility()) {
if (legacyModelSection != null) {
try {
LegacyItemModel legacyItemModel = LegacyItemModel.fromMap(legacyModelSection, customModelData);
for (ModelGeneration generation : legacyItemModel.modelsToGenerate()) {
prepareModelGeneration(generation);
}
legacyOverridesModels = new TreeSet<>(legacyItemModel.overrides());
} catch (LocalizedResourceConfigException e) {
collector.addAndThrow(e);
}
} else {
legacyOverridesModels = new TreeSet<>();
processModelRecursively(modernModel, new LinkedHashMap<>(), legacyOverridesModels, clientBoundMaterial, customModelData);
if (legacyOverridesModels.isEmpty()) {
collector.add(new LocalizedResourceConfigException("warning.config.item.legacy_model.cannot_convert"));
}
}
}
}
// 自定义物品的model处理
if (!isVanillaItem) {
boolean hasLegacyModel = legacyOverridesModels != null && !legacyOverridesModels.isEmpty();
boolean hasModernModel = modernModel != null;
// 自定义物品的model处理
// 这个item-model是否存在且是原版item-model
boolean isVanillaItemModel = itemModelKey != null && AbstractPackManager.PRESET_ITEMS.containsKey(itemModelKey);
boolean isVanillaItemModel = itemModel != null && AbstractPackManager.PRESET_ITEMS.containsKey(itemModel);
// 使用了自定义模型值
if (customModelData != 0) {
// 如果用户主动设置了item-model且为原版物品则使用item-model为基础模型否则使用其视觉材质对应的item-model
Key finalBaseModel = isVanillaItemModel ? itemModelKey : clientBoundMaterial;
// 检查cmd冲突
Map<Integer, Key> conflict = AbstractItemManager.this.cmdConflictChecker.computeIfAbsent(finalBaseModel, k -> new HashMap<>());
if (conflict.containsKey(customModelData)) {
collector.addAndThrow(new LocalizedResourceConfigException("warning.config.item.custom_model_data_conflict", String.valueOf(customModelData), conflict.get(customModelData).toString()));
}
conflict.put(customModelData, id);
Key finalBaseModel = isVanillaItemModel ? itemModel : clientBoundMaterial;
// 添加新版item model
if (isModernFormatRequired() && modernModel != null) {
if (isModernFormatRequired() && hasModernModel) {
TreeMap<Integer, ModernItemModel> map = AbstractItemManager.this.modernOverrides.computeIfAbsent(finalBaseModel, k -> new TreeMap<>());
map.put(customModelData, new ModernItemModel(
modernModel,
@@ -552,41 +683,33 @@ public abstract class AbstractItemManager<I> extends AbstractModelGenerator impl
));
}
// 添加旧版 overrides
if (needsLegacyCompatibility() && legacyOverridesModels != null && !legacyOverridesModels.isEmpty()) {
if (needsLegacyCompatibility() && hasLegacyModel) {
TreeSet<LegacyOverridesModel> lom = AbstractItemManager.this.legacyOverrides.computeIfAbsent(finalBaseModel, k -> new TreeSet<>());
lom.addAll(legacyOverridesModels);
}
} else if (isVanillaItemModel) {
collector.addAndThrow(new LocalizedResourceConfigException("warning.config.item.item_model.conflict", itemModelKey.asString()));
collector.addAndThrow(new LocalizedResourceConfigException("warning.config.item.item_model.conflict", itemModel.asString()));
}
// 使用了item-model组件且不是原版物品的
if (itemModelKey != null && !isVanillaItemModel) {
if (isModernFormatRequired() && modernModel != null) {
AbstractItemManager.this.modernItemModels1_21_4.put(itemModelKey, new ModernItemModel(
if (itemModel != null && !isVanillaItemModel) {
if (isModernFormatRequired() && hasModernModel) {
AbstractItemManager.this.modernItemModels1_21_4.put(itemModel, new ModernItemModel(
modernModel,
ResourceConfigUtils.getAsBoolean(section.getOrDefault("oversized-in-gui", true), "oversized-in-gui"),
ResourceConfigUtils.getAsBoolean(section.getOrDefault("hand-animation-on-swap", true), "hand-animation-on-swap")
));
}
if (Config.packMaxVersion().isAtOrAbove(MinecraftVersions.V1_21_2) && needsLegacyCompatibility() && legacyOverridesModels != null && !legacyOverridesModels.isEmpty()) {
TreeSet<LegacyOverridesModel> lom = AbstractItemManager.this.modernItemModels1_21_2.computeIfAbsent(itemModelKey, k -> new TreeSet<>());
if (needsItemModelCompatibility() && needsLegacyCompatibility() && hasLegacyModel) {
TreeSet<LegacyOverridesModel> lom = AbstractItemManager.this.modernItemModels1_21_2.computeIfAbsent(itemModel, k -> new TreeSet<>());
lom.addAll(legacyOverridesModels);
}
}
} else {
// 原版物品的item model覆写
if (isModernFormatRequired()) {
AbstractItemManager.this.modernItemModels1_21_4.put(id, new ModernItemModel(
modernModel,
ResourceConfigUtils.getAsBoolean(section.getOrDefault("oversized-in-gui", true), "oversized-in-gui"),
ResourceConfigUtils.getAsBoolean(section.getOrDefault("hand-animation-on-swap", true), "hand-animation-on-swap")
));
}
}
// 抛出异常
collector.throwIfPresent();
// 抛出异常
collector.throwIfPresent();
}, () -> GsonHelper.get().toJson(section)));
}
}

View File

@@ -62,11 +62,6 @@ public final class ItemKeys {
public static final Key BLACK_DYE = Key.of("minecraft:black_dye");
public static final Key FIREWORK_STAR = Key.of("minecraft:firework_star");
public static final Key[] AXES = new Key[] {
WOODEN_AXE, STONE_AXE, IRON_AXE, GOLDEN_AXE, DIAMOND_AXE, NETHERITE_AXE
};
public static final Key[] WATER_BUCKETS = new Key[] {
WATER_BUCKET, COD_BUCKET, SALMON_BUCKET, TROPICAL_FISH_BUCKET, TADPOLE_BUCKET, PUFFERFISH_BUCKET, AXOLOTL_BUCKET
};
public static final Key[] AXES = new Key[] {WOODEN_AXE, STONE_AXE, IRON_AXE, GOLDEN_AXE, DIAMOND_AXE, NETHERITE_AXE};
public static final Key[] WATER_BUCKETS = new Key[] {WATER_BUCKET, COD_BUCKET, SALMON_BUCKET, TROPICAL_FISH_BUCKET, TADPOLE_BUCKET, PUFFERFISH_BUCKET, AXOLOTL_BUCKET};
}

View File

@@ -102,7 +102,9 @@ public interface ItemManager<T> extends Manageable, ModelGenerator {
int fuelTime(Key id);
Collection<Suggestion> cachedSuggestions();
Collection<Suggestion> cachedCustomItemSuggestions();
Collection<Suggestion> cachedAllItemSuggestions();
Collection<Suggestion> cachedTotemSuggestions();

View File

@@ -3,7 +3,6 @@ package net.momirealms.craftengine.core.item;
import net.momirealms.craftengine.core.entity.Billboard;
import net.momirealms.craftengine.core.entity.ItemDisplayContext;
import net.momirealms.craftengine.core.entity.projectile.ProjectileMeta;
import net.momirealms.craftengine.core.entity.projectile.ProjectileType;
import net.momirealms.craftengine.core.item.equipment.ComponentBasedEquipment;
import net.momirealms.craftengine.core.item.equipment.Equipment;
import net.momirealms.craftengine.core.item.modifier.EquippableModifier;
@@ -408,9 +407,8 @@ public class ItemSettings {
Vector3f translation = ResourceConfigUtils.getAsVector3f(args.getOrDefault("translation", "0"), "translation");
Vector3f scale = ResourceConfigUtils.getAsVector3f(args.getOrDefault("scale", "1"), "scale");
Quaternionf rotation = ResourceConfigUtils.getAsQuaternionf(ResourceConfigUtils.get(args, "rotation"), "rotation");
ProjectileType type = Optional.ofNullable(args.get("type")).map(String::valueOf).map(it -> ProjectileType.valueOf(it.toUpperCase(Locale.ENGLISH))).orElse(null);
double range = ResourceConfigUtils.getAsDouble(args.getOrDefault("range", 1), "range");
return settings -> settings.projectileMeta(new ProjectileMeta(customTridentItemId, displayType, billboard, scale, translation, rotation, range, type));
return settings -> settings.projectileMeta(new ProjectileMeta(customTridentItemId, displayType, billboard, scale, translation, rotation, range));
}));
registerFactory("helmet", (value -> {
Map<String, Object> args = MiscUtils.castToMap(value, false);

View File

@@ -13,7 +13,7 @@ public class EmptyItemBehavior extends ItemBehavior {
public static class Factory implements ItemBehaviorFactory {
@Override
public ItemBehavior create(Pack pack, Path path, Key id, Map<String, Object> arguments) {
public ItemBehavior create(Pack pack, Path path, String node, Key id, Map<String, Object> arguments) {
return INSTANCE;
}
}

View File

@@ -8,5 +8,5 @@ import java.util.Map;
public interface ItemBehaviorFactory {
ItemBehavior create(Pack pack, Path path, Key id, Map<String, Object> arguments);
ItemBehavior create(Pack pack, Path path, String node, Key id, Map<String, Object> arguments);
}

View File

@@ -6,13 +6,10 @@ import net.momirealms.craftengine.core.registry.BuiltInRegistries;
import net.momirealms.craftengine.core.registry.Registries;
import net.momirealms.craftengine.core.registry.WritableRegistry;
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.util.ResourceKey;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class ItemBehaviors {
@@ -23,7 +20,7 @@ public class ItemBehaviors {
.register(ResourceKey.create(Registries.ITEM_BEHAVIOR_FACTORY.location(), key), factory);
}
public static ItemBehavior fromMap(Pack pack, Path path, Key id, Map<String, Object> map) {
public static ItemBehavior fromMap(Pack pack, Path path, String node, Key id, Map<String, Object> map) {
if (map == null || map.isEmpty()) return EmptyItemBehavior.INSTANCE;
String type = ResourceConfigUtils.requireNonEmptyStringOrThrow(map.get("type"), "warning.config.item.behavior.missing_type");
Key key = Key.withDefaultNamespace(type, Key.DEFAULT_NAMESPACE);
@@ -31,25 +28,6 @@ public class ItemBehaviors {
if (factory == null) {
throw new LocalizedResourceConfigException("warning.config.item.behavior.invalid_type", type);
}
return factory.create(pack, path, id, map);
}
public static List<ItemBehavior> fromList(Pack pack, Path path, Key id, List<Map<String, Object>> list) {
List<ItemBehavior> behaviors = new ArrayList<>(list.size());
for (Map<String, Object> map : list) {
behaviors.add(fromMap(pack, path, id, map));
}
return behaviors;
}
@SuppressWarnings("unchecked")
public static List<ItemBehavior> fromObj(Pack pack, Path path, Key id, Object behaviorObj) {
if (behaviorObj instanceof Map<?,?>) {
return List.of(fromMap(pack, path, id, MiscUtils.castToMap(behaviorObj, false)));
} else if (behaviorObj instanceof List<?>) {
return fromList(pack, path, id, (List<Map<String, Object>>) behaviorObj);
} else {
return List.of();
}
return factory.create(pack, path, node, id, map);
}
}

View File

@@ -6,6 +6,7 @@ import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.config.Config;
import net.momirealms.craftengine.core.plugin.config.ConfigParser;
import net.momirealms.craftengine.core.plugin.config.IdSectionConfigParser;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.UniqueKey;
@@ -16,7 +17,7 @@ import java.util.*;
public abstract class AbstractRecipeManager<T> implements RecipeManager<T> {
protected final Map<RecipeType, List<Recipe<T>>> byType = new EnumMap<>(RecipeType.class);
protected final Map<Key, Recipe<T>> byId = new HashMap<>();
protected final Map<Key, Recipe<T>> byId = new LinkedHashMap<>();
protected final Map<Key, List<Recipe<T>>> byResult = new HashMap<>();
protected final Map<Key, List<Recipe<T>>> byIngredient = new HashMap<>();
protected final Set<Key> dataPackRecipes = new HashSet<>();
@@ -106,19 +107,21 @@ public abstract class AbstractRecipeManager<T> implements RecipeManager<T> {
if (recipe instanceof AbstractedFixedResultRecipe<?> fixedResult) {
this.byResult.computeIfAbsent(fixedResult.result().item().id(), k -> new ArrayList<>()).add(recipe);
}
HashSet<Key> usedKeys = new HashSet<>();
for (Ingredient<T> ingredient : recipe.ingredientsInUse()) {
for (UniqueKey holder : ingredient.items()) {
Key key = holder.key();
if (usedKeys.add(key)) {
this.byIngredient.computeIfAbsent(key, k -> new ArrayList<>()).add(recipe);
if (recipe.canBeSearchedByIngredients()) {
HashSet<Key> usedKeys = new HashSet<>();
for (Ingredient<T> ingredient : recipe.ingredientsInUse()) {
for (UniqueKey holder : ingredient.items()) {
Key key = holder.key();
if (usedKeys.add(key)) {
this.byIngredient.computeIfAbsent(key, k -> new ArrayList<>()).add(recipe);
}
}
}
}
return true;
}
public class RecipeParser implements ConfigParser {
public class RecipeParser extends IdSectionConfigParser {
public static final String[] CONFIG_SECTION_NAME = new String[] {"recipes", "recipe"};
@Override
@@ -132,10 +135,10 @@ public abstract class AbstractRecipeManager<T> implements RecipeManager<T> {
}
@Override
public void parseSection(Pack pack, Path path, Key id, Map<String, Object> section) {
public void parseSection(Pack pack, Path path, String node, Key id, Map<String, Object> section) {
if (!Config.enableRecipeSystem()) return;
if (AbstractRecipeManager.this.byId.containsKey(id)) {
throw new LocalizedResourceConfigException("warning.config.recipe.duplicate", path, id);
throw new LocalizedResourceConfigException("warning.config.recipe.duplicate");
}
Recipe<T> recipe = RecipeSerializers.fromMap(id, section);
try {

View File

@@ -9,6 +9,12 @@ import net.momirealms.craftengine.core.item.recipe.result.CustomRecipeResult;
import net.momirealms.craftengine.core.item.recipe.result.PostProcessor;
import net.momirealms.craftengine.core.item.recipe.result.PostProcessors;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.context.Condition;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.context.condition.AllOfCondition;
import net.momirealms.craftengine.core.plugin.context.event.EventConditions;
import net.momirealms.craftengine.core.plugin.context.event.EventFunctions;
import net.momirealms.craftengine.core.plugin.context.function.Function;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.util.*;
import org.jetbrains.annotations.NotNull;
@@ -24,6 +30,23 @@ public abstract class AbstractRecipeSerializer<T, R extends Recipe<T>> implement
new VanillaRecipeReader1_20_5() :
new VanillaRecipeReader1_20();
@SuppressWarnings("unchecked")
protected Function<PlayerOptionalContext>[] functions(Map<String, Object> arguments) {
Object functions = ResourceConfigUtils.get(arguments, "functions", "function");
if (functions == null) return null;
List<Function<PlayerOptionalContext>> functionList = ResourceConfigUtils.parseConfigAsList(functions, EventFunctions::fromMap);
return functionList.toArray(new Function[0]);
}
protected Condition<PlayerOptionalContext> conditions(Map<String, Object> arguments) {
Object conditions = ResourceConfigUtils.get(arguments, "conditions", "condition");
if (conditions == null) return null;
List<Condition<PlayerOptionalContext>> conditionList = ResourceConfigUtils.parseConfigAsList(conditions, EventConditions::fromMap);
if (conditionList.isEmpty()) return null;
if (conditionList.size() == 1) return conditionList.getFirst();
return new AllOfCondition<>(conditionList);
}
protected boolean showNotification(Map<String, Object> arguments) {
return ResourceConfigUtils.getAsBoolean(arguments.getOrDefault("show-notification", true), "show-notification");
}

View File

@@ -0,0 +1,8 @@
package net.momirealms.craftengine.core.item.recipe;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
public interface ConditionalRecipe {
boolean canUse(final PlayerOptionalContext context);
}

View File

@@ -4,17 +4,37 @@ import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.item.ItemBuildContext;
import net.momirealms.craftengine.core.item.recipe.input.RecipeInput;
import net.momirealms.craftengine.core.item.recipe.result.CustomRecipeResult;
import net.momirealms.craftengine.core.plugin.context.Condition;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.context.function.Function;
import net.momirealms.craftengine.core.util.Key;
import org.jetbrains.annotations.Nullable;
public abstract class CustomCraftingTableRecipe<T> extends AbstractGroupedRecipe<T> {
public abstract class CustomCraftingTableRecipe<T> extends AbstractGroupedRecipe<T> implements ConditionalRecipe {
protected final CraftingRecipeCategory category;
private final CustomRecipeResult<T> visualResult;
private final Function<PlayerOptionalContext>[] craftingFunctions;
private final Condition<PlayerOptionalContext> craftingCondition;
protected CustomCraftingTableRecipe(Key id, boolean showNotification, CustomRecipeResult<T> result, @Nullable CustomRecipeResult<T> visualResult, String group, CraftingRecipeCategory category) {
protected CustomCraftingTableRecipe(Key id,
boolean showNotification,
CustomRecipeResult<T> result,
@Nullable CustomRecipeResult<T> visualResult,
String group,
CraftingRecipeCategory category,
Function<PlayerOptionalContext>[] craftingFunctions,
Condition<PlayerOptionalContext> craftingCondition) {
super(id, showNotification, result, group);
this.category = category == null ? CraftingRecipeCategory.MISC : category;
this.visualResult = visualResult;
this.craftingFunctions = craftingFunctions;
this.craftingCondition = craftingCondition;
}
@Override
public boolean canUse(PlayerOptionalContext context) {
if (this.craftingCondition == null) return true;
return this.craftingCondition.test(context);
}
public CraftingRecipeCategory category() {
@@ -49,4 +69,8 @@ public abstract class CustomCraftingTableRecipe<T> extends AbstractGroupedRecipe
return super.result.buildItem(context);
}
}
public Function<PlayerOptionalContext>[] craftingFunctions() {
return craftingFunctions;
}
}

View File

@@ -5,6 +5,9 @@ import com.google.gson.JsonObject;
import net.momirealms.craftengine.core.item.recipe.input.CraftingInput;
import net.momirealms.craftengine.core.item.recipe.input.RecipeInput;
import net.momirealms.craftengine.core.item.recipe.result.CustomRecipeResult;
import net.momirealms.craftengine.core.plugin.context.Condition;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.context.function.Function;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MiscUtils;
@@ -24,8 +27,10 @@ public class CustomShapedRecipe<T> extends CustomCraftingTableRecipe<T> {
CustomRecipeResult<T> visualResult,
String group,
CraftingRecipeCategory category,
Pattern<T> pattern) {
super(id, showNotification, result, visualResult, group, category);
Pattern<T> pattern,
Function<PlayerOptionalContext>[] craftingFunctions,
Condition<PlayerOptionalContext> craftingCondition) {
super(id, showNotification, result, visualResult, group, category, craftingFunctions, craftingCondition);
this.pattern = pattern;
this.parsedPattern = pattern.parse();
}
@@ -169,7 +174,9 @@ public class CustomShapedRecipe<T> extends CustomCraftingTableRecipe<T> {
parseResult(arguments),
parseVisualResult(arguments),
arguments.containsKey("group") ? arguments.get("group").toString() : null, craftingRecipeCategory(arguments),
new Pattern<>(pattern.toArray(new String[0]), ingredients)
new Pattern<>(pattern.toArray(new String[0]), ingredients),
functions(arguments),
conditions(arguments)
);
}
@@ -182,7 +189,9 @@ public class CustomShapedRecipe<T> extends CustomCraftingTableRecipe<T> {
null,
VANILLA_RECIPE_HELPER.readGroup(json),
VANILLA_RECIPE_HELPER.craftingCategory(json),
new Pattern<>(VANILLA_RECIPE_HELPER.craftingShapedPattern(json), ingredients)
new Pattern<>(VANILLA_RECIPE_HELPER.craftingShapedPattern(json), ingredients),
null,
null
);
}

View File

@@ -4,6 +4,9 @@ import com.google.gson.JsonObject;
import net.momirealms.craftengine.core.item.recipe.input.CraftingInput;
import net.momirealms.craftengine.core.item.recipe.input.RecipeInput;
import net.momirealms.craftengine.core.item.recipe.result.CustomRecipeResult;
import net.momirealms.craftengine.core.plugin.context.Condition;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.context.function.Function;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MiscUtils;
import org.jetbrains.annotations.NotNull;
@@ -23,8 +26,10 @@ public class CustomShapelessRecipe<T> extends CustomCraftingTableRecipe<T> {
CustomRecipeResult<T> visualResult,
String group,
CraftingRecipeCategory category,
List<Ingredient<T>> ingredients) {
super(id, showNotification, result, visualResult, group, category);
List<Ingredient<T>> ingredients,
Function<PlayerOptionalContext>[] craftingFunctions,
Condition<PlayerOptionalContext> craftingCondition) {
super(id, showNotification, result, visualResult, group, category, craftingFunctions, craftingCondition);
this.ingredients = ingredients;
this.placementInfo = PlacementInfo.create(ingredients);
}
@@ -88,7 +93,9 @@ public class CustomShapelessRecipe<T> extends CustomCraftingTableRecipe<T> {
parseResult(arguments),
parseVisualResult(arguments),
arguments.containsKey("group") ? arguments.get("group").toString() : null, craftingRecipeCategory(arguments),
ingredients
ingredients,
functions(arguments),
conditions(arguments)
);
}
@@ -99,7 +106,9 @@ public class CustomShapelessRecipe<T> extends CustomCraftingTableRecipe<T> {
parseResult(VANILLA_RECIPE_HELPER.craftingResult(json.getAsJsonObject("result"))),
null,
VANILLA_RECIPE_HELPER.readGroup(json), VANILLA_RECIPE_HELPER.craftingCategory(json),
VANILLA_RECIPE_HELPER.shapelessIngredients(json.getAsJsonArray("ingredients")).stream().map(this::toIngredient).toList()
VANILLA_RECIPE_HELPER.shapelessIngredients(json.getAsJsonArray("ingredients")).stream().map(this::toIngredient).toList(),
null,
null
);
}
}

View File

@@ -7,6 +7,8 @@ import net.momirealms.craftengine.core.item.recipe.input.RecipeInput;
import net.momirealms.craftengine.core.item.recipe.input.SmithingInput;
import net.momirealms.craftengine.core.item.recipe.result.CustomRecipeResult;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.context.Condition;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.registry.BuiltInRegistries;
import net.momirealms.craftengine.core.registry.Registries;
@@ -20,13 +22,14 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
public class CustomSmithingTransformRecipe<T> extends AbstractedFixedResultRecipe<T> {
public class CustomSmithingTransformRecipe<T> extends AbstractedFixedResultRecipe<T> implements ConditionalRecipe {
public static final Serializer<?> SERIALIZER = new Serializer<>();
private final Ingredient<T> base;
private final Ingredient<T> template;
private final Ingredient<T> addition;
private final boolean mergeComponents;
private final List<ItemDataProcessor> processors;
private final Condition<PlayerOptionalContext> condition;
public CustomSmithingTransformRecipe(Key id,
boolean showNotification,
@@ -35,7 +38,8 @@ public class CustomSmithingTransformRecipe<T> extends AbstractedFixedResultRecip
@Nullable Ingredient<T> addition,
CustomRecipeResult<T> result,
List<ItemDataProcessor> processors,
boolean mergeComponents
boolean mergeComponents,
Condition<PlayerOptionalContext> condition
) {
super(id, showNotification, result);
this.base = base;
@@ -43,6 +47,13 @@ public class CustomSmithingTransformRecipe<T> extends AbstractedFixedResultRecip
this.addition = addition;
this.processors = processors;
this.mergeComponents = mergeComponents;
this.condition = condition;
}
@Override
public boolean canUse(PlayerOptionalContext context) {
if (this.condition != null) return this.condition.test(context);
return true;
}
@SuppressWarnings("unchecked")
@@ -140,14 +151,23 @@ public class CustomSmithingTransformRecipe<T> extends AbstractedFixedResultRecip
toIngredient(addition),
parseResult(arguments),
ItemDataProcessors.fromMapList(processors),
mergeComponents
mergeComponents,
conditions(arguments)
);
}
@Override
public CustomSmithingTransformRecipe<A> readJson(Key id, JsonObject json) {
return new CustomSmithingTransformRecipe<>(id,
true, toIngredient(VANILLA_RECIPE_HELPER.singleIngredient(json.get("template"))), Objects.requireNonNull(toIngredient(VANILLA_RECIPE_HELPER.singleIngredient(json.get("base")))), toIngredient(VANILLA_RECIPE_HELPER.singleIngredient(json.get("addition"))), parseResult(VANILLA_RECIPE_HELPER.smithingResult(json.getAsJsonObject("result"))), null, true
return new CustomSmithingTransformRecipe<>(
id,
true,
toIngredient(VANILLA_RECIPE_HELPER.singleIngredient(json.get("template"))),
Objects.requireNonNull(toIngredient(VANILLA_RECIPE_HELPER.singleIngredient(json.get("base")))),
toIngredient(VANILLA_RECIPE_HELPER.singleIngredient(json.get("addition"))),
parseResult(VANILLA_RECIPE_HELPER.smithingResult(json.getAsJsonObject("result"))),
null,
true,
null
);
}
}

View File

@@ -6,6 +6,8 @@ import net.momirealms.craftengine.core.item.ItemBuildContext;
import net.momirealms.craftengine.core.item.recipe.input.RecipeInput;
import net.momirealms.craftengine.core.item.recipe.input.SmithingInput;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.context.Condition;
import net.momirealms.craftengine.core.plugin.context.PlayerOptionalContext;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.util.ResourceConfigUtils;
@@ -18,31 +20,41 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
public class CustomSmithingTrimRecipe<T> extends AbstractRecipe<T> {
public class CustomSmithingTrimRecipe<T> extends AbstractRecipe<T> implements ConditionalRecipe {
public static final Serializer<?> SERIALIZER = new Serializer<>();
private final Ingredient<T> base;
private final Ingredient<T> template;
private final Ingredient<T> addition;
@Nullable // 1.21.5
private final Key pattern;
@Nullable
private final Condition<PlayerOptionalContext> condition;
public CustomSmithingTrimRecipe(@NotNull Key id,
boolean showNotification,
@NotNull Ingredient<T> template,
@NotNull Ingredient<T> base,
@NotNull Ingredient<T> addition,
@Nullable Key pattern
@Nullable Key pattern,
@Nullable Condition<PlayerOptionalContext> condition
) {
super(id, showNotification);
this.base = base;
this.template = template;
this.addition = addition;
this.pattern = pattern;
this.condition = condition;
if (pattern == null && VersionHelper.isOrAbove1_21_5()) {
throw new IllegalStateException("SmithingTrimRecipe cannot have a null pattern on 1.21.5 and above.");
}
}
@Override
public boolean canUse(PlayerOptionalContext context) {
if (this.condition != null) return this.condition.test(context);
return true;
}
@SuppressWarnings("unchecked")
@Override
public T assemble(RecipeInput input, ItemBuildContext context) {
@@ -103,6 +115,11 @@ public class CustomSmithingTrimRecipe<T> extends AbstractRecipe<T> {
return pattern;
}
@Override
public boolean canBeSearchedByIngredients() {
return false;
}
@SuppressWarnings({"DuplicatedCode"})
public static class Serializer<A> extends AbstractRecipeSerializer<A, CustomSmithingTrimRecipe<A>> {
@@ -117,7 +134,8 @@ public class CustomSmithingTrimRecipe<T> extends AbstractRecipe<T> {
ResourceConfigUtils.requireNonNullOrThrow(toIngredient(template), "warning.config.recipe.smithing_trim.missing_template_type"),
ResourceConfigUtils.requireNonNullOrThrow(toIngredient(base), "warning.config.recipe.smithing_trim.missing_base"),
ResourceConfigUtils.requireNonNullOrThrow(toIngredient(addition), "warning.config.recipe.smithing_trim.missing_addition"),
pattern
pattern,
conditions(arguments)
);
}
@@ -128,7 +146,8 @@ public class CustomSmithingTrimRecipe<T> extends AbstractRecipe<T> {
Objects.requireNonNull(toIngredient(VANILLA_RECIPE_HELPER.singleIngredient(json.get("template")))),
Objects.requireNonNull(toIngredient(VANILLA_RECIPE_HELPER.singleIngredient(json.get("base")))),
Objects.requireNonNull(toIngredient(VANILLA_RECIPE_HELPER.singleIngredient(json.get("addition")))),
VersionHelper.isOrAbove1_21_5() ? Key.of(json.get("pattern").getAsString()) : null
VersionHelper.isOrAbove1_21_5() ? Key.of(json.get("pattern").getAsString()) : null,
null
);
}
}

View File

@@ -25,4 +25,8 @@ public interface Recipe<T> {
default boolean showNotification() {
return true;
}
default boolean canBeSearchedByIngredients() {
return true;
}
}

View File

@@ -0,0 +1,87 @@
package net.momirealms.craftengine.core.item.trade;
import net.momirealms.craftengine.core.item.Item;
import java.util.Optional;
import java.util.function.Function;
public class MerchantOffer<I> {
private Item<I> cost1;
private Optional<Item<I>> cost2;
private Item<I> result;
private final int uses;
private final int maxUses;
private final int specialPrice;
private final int xp;
private final float priceMultiplier;
private final int demand;
private final boolean outOfStock;
public MerchantOffer(Item<I> cost1,
Optional<Item<I>> cost2,
Item<I> result,
boolean outOfStock,
int uses,
int maxUses,
int xp,
int specialPrice,
float priceMultiplier,
int demand) {
this.cost1 = cost1;
this.cost2 = cost2;
this.result = result;
this.outOfStock = outOfStock;
this.uses = uses;
this.maxUses = maxUses;
this.specialPrice = specialPrice;
this.xp = xp;
this.priceMultiplier = priceMultiplier;
this.demand = demand;
}
public void applyClientboundData(Function<Item<I>, Item<I>> function) {
this.cost1 = function.apply(this.cost1);
this.cost2 = this.cost2.map(function);
this.result = function.apply(this.result);
}
public Item<I> cost1() {
return cost1;
}
public Optional<Item<I>> cost2() {
return cost2;
}
public Item<I> result() {
return result;
}
public int uses() {
return uses;
}
public int maxUses() {
return maxUses;
}
public int specialPrice() {
return specialPrice;
}
public int xp() {
return xp;
}
public float priceMultiplier() {
return priceMultiplier;
}
public int demand() {
return demand;
}
public boolean outOfStock() {
return outOfStock;
}
}

View File

@@ -8,7 +8,7 @@ import net.momirealms.craftengine.core.loot.function.LootFunction;
import net.momirealms.craftengine.core.loot.function.LootFunctions;
import net.momirealms.craftengine.core.plugin.context.Condition;
import net.momirealms.craftengine.core.plugin.context.number.NumberProvider;
import net.momirealms.craftengine.core.util.MCUtils;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.util.MutableInt;
import net.momirealms.craftengine.core.util.RandomUtils;
@@ -44,7 +44,7 @@ public class LootPool<T> {
}
if (this.compositeCondition.test(context)) {
Consumer<Item<T>> consumer = LootFunction.decorate(this.compositeFunction, lootConsumer, context);
int i = this.rolls.getInt(context) + MCUtils.fastFloor(this.bonusRolls.getFloat(context) * context.luck());
int i = this.rolls.getInt(context) + MiscUtils.fastFloor(this.bonusRolls.getFloat(context) * context.luck());
for (int j = 0; j < i; ++j) {
this.addRandomItem(createFunctionApplier(consumer, context), context);
}

View File

@@ -3,7 +3,7 @@ package net.momirealms.craftengine.core.loot.entry;
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.MCUtils;
import net.momirealms.craftengine.core.util.MiscUtils;
import java.util.List;
import java.util.function.Predicate;
@@ -14,7 +14,7 @@ public abstract class AbstractLootEntryContainer<T> implements LootEntryContaine
protected AbstractLootEntryContainer(List<Condition<LootContext>> conditions) {
this.conditions = conditions;
this.compositeCondition = MCUtils.allOf(conditions);
this.compositeCondition = MiscUtils.allOf(conditions);
}
@Override

View File

@@ -3,7 +3,7 @@ package net.momirealms.craftengine.core.loot.function;
import net.momirealms.craftengine.core.item.Item;
import net.momirealms.craftengine.core.loot.LootContext;
import net.momirealms.craftengine.core.plugin.context.Condition;
import net.momirealms.craftengine.core.util.MCUtils;
import net.momirealms.craftengine.core.util.MiscUtils;
import java.util.List;
import java.util.function.Predicate;
@@ -14,7 +14,7 @@ public abstract class AbstractLootConditionalFunction<T> implements LootFunction
public AbstractLootConditionalFunction(List<Condition<LootContext>> predicates) {
this.predicates = predicates;
this.compositePredicates = MCUtils.allOf(predicates);
this.compositePredicates = MiscUtils.allOf(predicates);
}
@Override

View File

@@ -0,0 +1,65 @@
package net.momirealms.craftengine.core.loot.function;
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.plugin.context.number.NumberProvider;
import net.momirealms.craftengine.core.plugin.context.number.NumberProviders;
import net.momirealms.craftengine.core.util.Key;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class LimitCountFunction<T> extends AbstractLootConditionalFunction<T> {
public static final Factory<?> FACTORY = new Factory<>();
@Nullable
private final NumberProvider min;
@Nullable
private final NumberProvider max;
public LimitCountFunction(List<Condition<LootContext>> predicates, @Nullable NumberProvider min, @Nullable NumberProvider max) {
super(predicates);
this.min = min;
this.max = max;
}
@Override
public Key type() {
return LootFunctions.LIMIT_COUNT;
}
@Override
protected Item<T> applyInternal(Item<T> item, LootContext context) {
int amount = item.count();
if (min != null) {
int minAmount = min.getInt(context);
if (amount < minAmount) {
item.count(minAmount);
}
}
if (max != null) {
int maxAmount = max.getInt(context);
if (amount > maxAmount) {
item.count(maxAmount);
}
}
return item;
}
public static class Factory<A> implements LootFunctionFactory<A> {
@SuppressWarnings("unchecked")
@Override
public LootFunction<A> create(Map<String, Object> arguments) {
Object min = arguments.get("min");
Object max = arguments.get("max");
List<Condition<LootContext>> conditions = Optional.ofNullable(arguments.get("conditions"))
.map(it -> LootConditions.fromMapList((List<Map<String, Object>>) it))
.orElse(Collections.emptyList());
return new LimitCountFunction<>(conditions, min == null ? null : NumberProviders.fromObject(min), max == null ? null : NumberProviders.fromObject(max));
}
}
}

View File

@@ -20,12 +20,14 @@ public class LootFunctions {
public static final Key SET_COUNT = Key.from("craftengine:set_count");
public static final Key EXPLOSION_DECAY = Key.from("craftengine:explosion_decay");
public static final Key DROP_EXP = Key.from("craftengine:drop_exp");
public static final Key LIMIT_COUNT = Key.from("craftengine:limit_count");
static {
register(SET_COUNT, SetCountFunction.FACTORY);
register(EXPLOSION_DECAY, ExplosionDecayFunction.FACTORY);
register(APPLY_BONUS, ApplyBonusCountFunction.FACTORY);
register(DROP_EXP, DropExpFunction.FACTORY);
register(LIMIT_COUNT, LimitCountFunction.FACTORY);
}
public static <T> void register(Key key, LootFunctionFactory<T> factory) {

View File

@@ -5,7 +5,6 @@ import com.google.common.collect.Multimap;
import com.google.common.jimfs.Configuration;
import com.google.common.jimfs.Jimfs;
import com.google.gson.*;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.momirealms.craftengine.core.font.BitmapImage;
import net.momirealms.craftengine.core.font.Font;
import net.momirealms.craftengine.core.item.equipment.ComponentBasedEquipment;
@@ -88,6 +87,7 @@ public abstract class AbstractPackManager implements PackManager {
private final BiConsumer<Path, Path> generationEventDispatcher;
private final Map<String, Pack> loadedPacks = new HashMap<>();
private final Map<String, ConfigParser> sectionParsers = new HashMap<>();
private final TreeSet<ConfigParser> sortedParsers = new TreeSet<>();
private final JsonObject vanillaAtlas;
private Map<Path, CachedConfigFile> cachedConfigFiles = Collections.emptyMap();
private Map<Path, CachedAssetFile> cachedAssetFiles = Collections.emptyMap();
@@ -295,6 +295,7 @@ public abstract class AbstractPackManager implements PackManager {
for (String id : parser.sectionId()) {
this.sectionParsers.put(id, parser);
}
this.sortedParsers.add(parser);
return true;
}
@@ -322,6 +323,9 @@ public abstract class AbstractPackManager implements PackManager {
if (namespace.charAt(0) == '.') {
continue;
}
if (!ResourceLocation.isValidNamespace(namespace)) {
namespace = "minecraft";
}
Path metaFile = path.resolve("pack.yml");
String description = null;
String version = null;
@@ -374,6 +378,7 @@ public abstract class AbstractPackManager implements PackManager {
plugin.saveResource("resources/internal/configuration/fix_client_visual.yml");
plugin.saveResource("resources/internal/configuration/offset_chars.yml");
plugin.saveResource("resources/internal/configuration/gui.yml");
plugin.saveResource("resources/internal/configuration/mappings.yml");
plugin.saveResource("resources/internal/resourcepack/assets/minecraft/textures/font/offset/space_split.png");
plugin.saveResource("resources/internal/resourcepack/assets/minecraft/textures/font/gui/custom/item_browser.png");
plugin.saveResource("resources/internal/resourcepack/assets/minecraft/textures/font/gui/custom/category.png");
@@ -431,6 +436,7 @@ public abstract class AbstractPackManager implements PackManager {
plugin.saveResource("resources/default/configuration/blocks/topaz_ore.yml");
plugin.saveResource("resources/default/configuration/blocks/netherite_anvil.yml");
plugin.saveResource("resources/default/configuration/blocks/amethyst_torch.yml");
plugin.saveResource("resources/default/configuration/blocks/hami_melon.yml");
// assets
plugin.saveResource("resources/default/resourcepack/assets/minecraft/textures/font/image/emojis.png");
plugin.saveResource("resources/default/resourcepack/assets/minecraft/textures/block/custom/chinese_lantern.png");
@@ -537,12 +543,17 @@ public abstract class AbstractPackManager implements PackManager {
plugin.saveResource("resources/default/resourcepack/assets/minecraft/textures/gui/sprites/tooltip/topaz_background.png.mcmeta");
plugin.saveResource("resources/default/resourcepack/assets/minecraft/textures/gui/sprites/tooltip/topaz_frame.png");
plugin.saveResource("resources/default/resourcepack/assets/minecraft/textures/gui/sprites/tooltip/topaz_frame.png.mcmeta");
plugin.saveResource("resources/default/resourcepack/assets/minecraft/textures/block/custom/hami_melon.png");
plugin.saveResource("resources/default/resourcepack/assets/minecraft/textures/block/custom/hami_melon_bottom.png");
plugin.saveResource("resources/default/resourcepack/assets/minecraft/textures/block/custom/hami_melon_top.png");
plugin.saveResource("resources/default/resourcepack/assets/minecraft/textures/item/custom/hami_melon_slice.png");
plugin.saveResource("resources/default/resourcepack/assets/minecraft/textures/item/custom/hami_melon_seeds.png");
plugin.saveResource("resources/default/resourcepack/assets/minecraft/models/block/custom/fence_side.json");
}
private TreeMap<ConfigParser, List<CachedConfigSection>> updateCachedConfigFiles() {
TreeMap<ConfigParser, List<CachedConfigSection>> cachedConfigs = new TreeMap<>();
private void updateCachedConfigFiles() {
Map<Path, CachedConfigFile> previousFiles = this.cachedConfigFiles;
this.cachedConfigFiles = new Object2ObjectOpenHashMap<>(32);
this.cachedConfigFiles = new HashMap<>(64, 0.5f);
for (Pack pack : loadedPacks()) {
if (!pack.enabled()) continue;
Path configurationFolderPath = pack.configurationFolder();
@@ -587,9 +598,7 @@ public abstract class AbstractPackManager implements PackManager {
}
}
for (Map.Entry<String, Object> entry : cachedFile.config().entrySet()) {
processConfigEntry(entry, path, cachedFile.pack(), (p, c) ->
cachedConfigs.computeIfAbsent(p, k -> new ArrayList<>()).add(c)
);
processConfigEntry(entry, path, cachedFile.pack(), ConfigParser::addConfig);
}
}
return FileVisitResult.CONTINUE;
@@ -599,66 +608,28 @@ public abstract class AbstractPackManager implements PackManager {
this.plugin.logger().severe("Error while reading config file", e);
}
}
return cachedConfigs;
}
private void loadResourceConfigs(Predicate<ConfigParser> predicate) {
long o1 = System.nanoTime();
TreeMap<ConfigParser, List<CachedConfigSection>> cachedConfigs = this.updateCachedConfigFiles();
this.updateCachedConfigFiles();
long o2 = System.nanoTime();
this.plugin.logger().info("Loaded packs. Took " + String.format("%.2f", ((o2 - o1) / 1_000_000.0)) + " ms");
for (Map.Entry<ConfigParser, List<CachedConfigSection>> entry : cachedConfigs.entrySet()) {
ConfigParser parser = entry.getKey();
if (!predicate.test(parser)) continue;
for (ConfigParser parser : this.sortedParsers) {
if (!predicate.test(parser)) {
parser.clear();
continue;
}
long t1 = System.nanoTime();
parser.preProcess();
for (CachedConfigSection cached : entry.getValue()) {
for (Map.Entry<String, Object> configEntry : cached.config().entrySet()) {
String key = configEntry.getKey();
Key id = Key.withDefaultNamespace(key, cached.pack().namespace());
try {
if (parser.supportsParsingObject()) {
// do not apply templates
parser.parseObject(cached.pack(), cached.filePath(), id, configEntry.getValue());
} else {
if (configEntry.getValue() instanceof Map<?, ?> configSection0) {
Map<String, Object> config = castToMap(configSection0, false);
if ((boolean) config.getOrDefault("debug", false)) {
this.plugin.logger().info(GsonHelper.get().toJson(this.plugin.templateManager().applyTemplates(id, config)));
}
if ((boolean) config.getOrDefault("enable", true)) {
parser.parseSection(cached.pack(), cached.filePath(), id, MiscUtils.castToMap(this.plugin.templateManager().applyTemplates(id, config), false));
}
} else {
TranslationManager.instance().log("warning.config.structure.not_section", cached.filePath().toString(), cached.prefix() + "." + key, configEntry.getValue().getClass().getSimpleName());
}
}
} catch (LocalizedException e) {
printWarningRecursively(e, cached.filePath(), cached.prefix() + "." + key);
} catch (Exception e) {
this.plugin.logger().warn("Unexpected error loading file " + cached.filePath() + " - '" + parser.sectionId()[0] + "." + key + "'. Please find the cause according to the stacktrace or seek developer help. Additional info: " + GsonHelper.get().toJson(configEntry.getValue()), e);
}
}
}
parser.loadAll();
parser.postProcess();
parser.clear();
long t2 = System.nanoTime();
this.plugin.logger().info("Loaded " + parser.sectionId()[0] + " in " + String.format("%.2f", ((t2 - t1) / 1_000_000.0)) + " ms");
}
}
private void printWarningRecursively(LocalizedException e, Path path, String prefix) {
for (Throwable t : e.getSuppressed()) {
if (t instanceof LocalizedException suppressed) {
printWarningRecursively(suppressed, path, prefix);
}
}
if (e instanceof LocalizedResourceConfigException exception) {
exception.setPath(path);
exception.setId(prefix);
}
TranslationManager.instance().log(e.node(), e.arguments());
}
private void processConfigEntry(Map.Entry<String, Object> entry, Path path, Pack pack, BiConsumer<ConfigParser, CachedConfigSection> callback) {
if (entry.getValue() instanceof Map<?,?> typeSections0) {
String key = entry.getKey();
@@ -728,7 +699,7 @@ public abstract class AbstractPackManager implements PackManager {
this.plugin.logger().info("Validated resource pack in " + (time3 - time2) + "ms");
Path finalPath = resourcePackPath();
Files.createDirectories(finalPath.getParent());
if (!VersionHelper.PREMIUM) {
if (!VersionHelper.PREMIUM && Config.enableObfuscation()) {
Config.instance().setObf(false);
this.plugin.logger().warn("Resource pack obfuscation requires Premium Edition.");
}
@@ -845,9 +816,6 @@ public abstract class AbstractPackManager implements PackManager {
}
}
}
case "filter", "minecraft:filter" -> {
// todo filter
}
case "paletted_permutations", "minecraft:paletted_permutations" -> {
JsonArray textures = sourceJson.getAsJsonArray("textures");
if (textures == null) continue;
@@ -862,6 +830,9 @@ public abstract class AbstractPackManager implements PackManager {
}
}
}
case "filter", "minecraft:filter" -> {
// todo filter
}
}
}
}
@@ -887,28 +858,32 @@ public abstract class AbstractPackManager implements PackManager {
Set<Key> existingTextures = new HashSet<>(VANILLA_TEXTURES);
Map<String, String> directoryMapper = new HashMap<>();
processAtlas(this.vanillaAtlas, directoryMapper::put, existingTextures::add, texturesInAtlas::add);
Map<Path, JsonObject> allAtlas = new HashMap<>();
for (Path rootPath : rootPaths) {
Path assetsPath = rootPath.resolve("assets");
if (!Files.isDirectory(assetsPath)) continue;
Path atlasesFile = assetsPath.resolve("minecraft").resolve("atlases").resolve("blocks.json");
if (Files.exists(atlasesFile)) {
try {
JsonObject atlasJsonObject = GsonHelper.readJsonFile(atlasesFile).getAsJsonObject();
processAtlas(atlasJsonObject, directoryMapper::put, existingTextures::add, texturesInAtlas::add);
allAtlas.put(atlasesFile, atlasJsonObject);
} catch (IOException | JsonParseException e) {
TranslationManager.instance().log("warning.config.resource_pack.generation.malformatted_json", atlasesFile.toAbsolutePath().toString());
}
}
List<Path> namespaces;
try {
namespaces = FileUtils.collectNamespaces(assetsPath);
} catch (IOException e) {
plugin.logger().warn("Failed to collect namespaces for " + assetsPath.toAbsolutePath(), e);
this.plugin.logger().warn("Failed to collect namespaces for " + assetsPath.toAbsolutePath(), e);
return;
}
for (Path namespacePath : namespaces) {
Path atlasesFile = namespacePath.resolve("atlases").resolve("blocks.json");
if (Files.exists(atlasesFile)) {
try {
JsonObject atlasJsonObject = GsonHelper.readJsonFile(atlasesFile).getAsJsonObject();
processAtlas(atlasJsonObject, directoryMapper::put, existingTextures::add, texturesInAtlas::add);
} catch (IOException | JsonParseException e) {
TranslationManager.instance().log("warning.config.resource_pack.generation.malformatted_json", atlasesFile.toAbsolutePath().toString());
}
}
for (Path namespacePath : namespaces) {
Path fontPath = namespacePath.resolve("font");
if (Files.isDirectory(fontPath)) {
try {
@@ -1058,6 +1033,8 @@ public abstract class AbstractPackManager implements PackManager {
TranslationManager.instance().log("warning.config.resource_pack.generation.missing_block_model", entry.getValue().stream().distinct().toList().toString(), modelPath);
}
Set<Key> texturesToFix = new HashSet<>();
// 验证贴图是否存在
boolean enableObf = Config.enableObfuscation() && Config.enableRandomResourceLocation();
label: for (Map.Entry<Key, Collection<Key>> entry : imageToModels.asMap().entrySet()) {
@@ -1087,7 +1064,48 @@ public abstract class AbstractPackManager implements PackManager {
continue label;
}
}
TranslationManager.instance().log("warning.config.resource_pack.generation.texture_not_in_atlas", key.toString());
if (Config.fixTextureAtlas()) {
texturesToFix.add(key);
} else {
TranslationManager.instance().log("warning.config.resource_pack.generation.texture_not_in_atlas", key.toString());
}
}
}
if (Config.fixTextureAtlas() && !texturesToFix.isEmpty()) {
List<JsonObject> sourcesToAdd = new ArrayList<>();
for (Key toFix : texturesToFix) {
JsonObject source = new JsonObject();
source.addProperty("type", "single");
source.addProperty("resource", toFix.asString());
sourcesToAdd.add(source);
}
Path defaultAtlas = path.resolve("assets").resolve("minecraft").resolve("atlases").resolve("blocks.json");
if (!allAtlas.containsKey(defaultAtlas)) {
allAtlas.put(defaultAtlas, new JsonObject());
try {
Files.createDirectories(defaultAtlas.getParent());
} catch (IOException e) {
this.plugin.logger().warn("could not create default atlas directory", e);
}
}
for (Map.Entry<Path, JsonObject> atlas : allAtlas.entrySet()) {
JsonObject right = atlas.getValue();
JsonArray sources = right.getAsJsonArray("sources");
if (sources == null) {
sources = new JsonArray();
right.add("sources", sources);
}
for (JsonObject source : sourcesToAdd) {
sources.add(source);
}
try {
GsonHelper.writeJsonFile(right, atlas.getKey());
} catch (IOException e) {
this.plugin.logger().warn("Failed to write atlas to json file", e);
}
}
}
}
@@ -1715,7 +1733,7 @@ public abstract class AbstractPackManager implements PackManager {
soundJson = new JsonObject();
}
for (Map.Entry<Key, Key> mapper : plugin.blockManager().soundMapper().entrySet()) {
for (Map.Entry<Key, Key> mapper : plugin.blockManager().soundReplacements().entrySet()) {
Key originalKey = mapper.getKey();
JsonObject empty = new JsonObject();
empty.add("sounds", new JsonArray());
@@ -2181,9 +2199,9 @@ public abstract class AbstractPackManager implements PackManager {
}
private List<Pair<String, List<Path>>> updateCachedAssets(@NotNull PackCacheData cacheData, @Nullable FileSystem fs) throws IOException {
Map<String, List<Path>> conflictChecker = new Object2ObjectOpenHashMap<>(Math.max(128, this.cachedAssetFiles.size()));
Map<String, List<Path>> conflictChecker = new HashMap<>(Math.max(128, this.cachedAssetFiles.size()), 0.6f);
Map<Path, CachedAssetFile> previousFiles = this.cachedAssetFiles;
this.cachedAssetFiles = new Object2ObjectOpenHashMap<>(Math.max(128, this.cachedAssetFiles.size()));
this.cachedAssetFiles = new HashMap<>(Math.max(128, this.cachedAssetFiles.size()), 0.6f);
List<Path> folders = new ArrayList<>();
folders.addAll(loadedPacks().stream()

View File

@@ -3,13 +3,14 @@ package net.momirealms.craftengine.core.pack;
public final class LoadingSequence {
private LoadingSequence() {}
// 模板第一位
public static final int TEMPLATE = 0;
public static final int GLOBAL_VAR = 10;
public static final int LANG = 20;
public static final int BLOCK_STATE_MAPPING = 10;
public static final int GLOBAL_VAR = 20;
public static final int TRANSLATION = 30;
public static final int BLOCK = 40;
public static final int EQUIPMENT = 50;
public static final int ITEM = 60;
public static final int EQUIPMENT = 40;
public static final int ITEM = 50;
public static final int BLOCK = 60;
public static final int FURNITURE = 70;
public static final int IMAGE = 80;
public static final int RECIPE = 90;
@@ -19,4 +20,5 @@ public final class LoadingSequence {
public static final int VANILLA_LOOTS = 130;
public static final int EMOJI = 140;
public static final int ADVANCEMENT = 150;
public static final int LANG = 160;
}

View File

@@ -0,0 +1,9 @@
package net.momirealms.craftengine.core.pack;
import net.momirealms.craftengine.core.util.Key;
import java.nio.file.Path;
import java.util.Map;
public record PendingConfigSection(Pack pack, Path path, String node, Key id, Map<String, Object> config) {
}

View File

@@ -0,0 +1,24 @@
package net.momirealms.craftengine.core.pack.allocator;
import net.momirealms.craftengine.core.block.BlockStateWrapper;
public class BlockStateCandidate {
private final BlockStateWrapper blockState;
private boolean used = false;
public BlockStateCandidate(BlockStateWrapper blockState) {
this.blockState = blockState;
}
public void setUsed() {
this.used = true;
}
public boolean isUsed() {
return used;
}
public BlockStateWrapper blockState() {
return blockState;
}
}

View File

@@ -0,0 +1,291 @@
package net.momirealms.craftengine.core.pack.allocator;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import net.momirealms.craftengine.core.util.FileUtils;
import net.momirealms.craftengine.core.util.GsonHelper;
import net.momirealms.craftengine.core.util.Pair;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
public class IdAllocator {
private final Path cacheFilePath;
private final BiMap<String, Integer> forcedIdMap = HashBiMap.create(128);
private final Map<String, Integer> cachedIdMap = new HashMap<>();
private final BitSet occupiedIdSet = new BitSet();
private final Map<String, CompletableFuture<Integer>> pendingAllocations = new LinkedHashMap<>();
private int nextAutoId;
private int minId;
private int maxId;
public IdAllocator(Path cacheFilePath) {
this.cacheFilePath = cacheFilePath;
}
/**
* 重置分配器状态
* @param minId 最小ID包含
* @param maxId 最大ID包含
*/
public void reset(int minId, int maxId) {
this.minId = minId;
this.nextAutoId = minId;
this.maxId = maxId;
this.occupiedIdSet.clear();
this.forcedIdMap.clear();
this.pendingAllocations.clear();
this.cachedIdMap.clear();
}
/**
* 处理所有待分配的自动ID请求
*/
public void processPendingAllocations() {
for (Map.Entry<String, Integer> entry : this.cachedIdMap.entrySet()) {
CompletableFuture<Integer> future = this.pendingAllocations.get(entry.getKey());
if (future != null) {
int id = entry.getValue();
if (!isIdAvailable(id)) {
continue;
}
allocateId(id, future);
} else {
// 避免其他条目分配到过时的值上
this.occupiedIdSet.set(entry.getValue());
}
}
for (Map.Entry<String, CompletableFuture<Integer>> entry : this.pendingAllocations.entrySet()) {
String name = entry.getKey();
CompletableFuture<Integer> future = entry.getValue();
if (future.isDone()) {
continue; // 已经在前面分配过了
}
// 分配新的自动ID
int newId = findNextAvailableId();
if (newId == -1) {
future.completeExceptionally(new IdExhaustedException(name, this.minId, this.maxId));
continue;
}
allocateId(newId, future);
this.cachedIdMap.put(name, newId);
}
this.pendingAllocations.clear();
}
private boolean isIdAvailable(Integer id) {
return id != null && id >= this.minId && id <= this.maxId
&& !this.occupiedIdSet.get(id);
}
private void allocateId(int id, CompletableFuture<Integer> future) {
this.occupiedIdSet.set(id);
future.complete(id);
}
private int findNextAvailableId() {
if (this.nextAutoId > this.maxId) {
return -1;
}
this.nextAutoId = this.occupiedIdSet.nextClearBit(this.nextAutoId);
return this.nextAutoId <= this.maxId ? this.nextAutoId : -1;
}
/**
* 强制分配指定ID无视限制
* @param name 名称
* @param id 要分配的ID
* @return 分配结果的Future
*/
public CompletableFuture<Integer> assignFixedId(String name, int id) {
// 检查ID是否被其他名称占用
String existingOwner = this.forcedIdMap.inverse().get(id);
if (existingOwner != null && !existingOwner.equals(name)) {
return CompletableFuture.failedFuture(new IdConflictException(existingOwner, id));
}
this.forcedIdMap.put(name, id);
this.cachedIdMap.remove(name); // 清除可能的缓存
this.occupiedIdSet.set(id);
return CompletableFuture.completedFuture(id);
}
public boolean isForced(String name) {
return this.forcedIdMap.containsKey(name);
}
public List<Pair<String, Integer>> getFixedIdsBetween(int minId, int maxId) {
BiMap<Integer, String> inverse = this.forcedIdMap.inverse();
List<Pair<String, Integer>> result = new ArrayList<>();
for (int i = minId; i <= maxId; i++) {
String s = inverse.get(i);
if (s != null) {
result.add(Pair.of(s, i));
}
}
return result;
}
/**
* 请求自动分配ID
* @param name 名称
* @return 分配结果的Future
*/
public CompletableFuture<Integer> requestAutoId(String name) {
CompletableFuture<Integer> future = new CompletableFuture<>();
this.pendingAllocations.put(name, future);
return future;
}
/**
* 清理不再使用的ID
* @param shouldRemove 判断是否应该移除的谓词
* @return 被移除的ID数量
*/
public List<String> cleanupUnusedIds(Predicate<String> shouldRemove) {
List<String> idsToRemove = new ArrayList<>();
for (String id : this.cachedIdMap.keySet()) {
if (shouldRemove.test(id)) {
idsToRemove.add(id);
}
}
for (String id : idsToRemove) {
Integer removedId = this.cachedIdMap.remove(id);
if (removedId != null && !this.forcedIdMap.containsValue(removedId)) {
this.occupiedIdSet.clear(removedId);
}
}
return idsToRemove;
}
/**
* 获取指定名称的ID
* @param name 名称
* @return ID如果不存在返回null
*/
public Integer getId(String name) {
Integer forcedId = this.forcedIdMap.get(name);
return forcedId != null ? forcedId : this.cachedIdMap.get(name);
}
/**
* 获取所有已分配的ID映射不可修改
*/
public Map<String, Integer> getAllAllocatedIds() {
Map<String, Integer> result = new HashMap<>();
result.putAll(this.forcedIdMap);
result.putAll(this.cachedIdMap);
return Collections.unmodifiableMap(result);
}
/**
* 检查ID是否已被占用
*/
public boolean isIdOccupied(int id) {
return this.occupiedIdSet.get(id);
}
/**
* 从文件加载缓存
*/
public void loadFromCache() throws IOException {
if (!Files.exists(this.cacheFilePath)) {
return;
}
JsonElement element = GsonHelper.readJsonFile(this.cacheFilePath);
if (element instanceof JsonObject jsonObject) {
for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) {
if (entry.getValue() instanceof JsonPrimitive primitive) {
int id = primitive.getAsInt();
this.cachedIdMap.put(entry.getKey(), id);
}
}
}
}
/**
* 保存缓存到文件
*/
public void saveToCache() throws IOException {
// 创建按ID排序的TreeMap
Map<Integer, String> sortedById = new TreeMap<>();
for (Map.Entry<String, Integer> entry : this.cachedIdMap.entrySet()) {
sortedById.put(entry.getValue(), entry.getKey());
}
// 创建有序的JSON对象
JsonObject sortedJsonObject = new JsonObject();
for (Map.Entry<Integer, String> entry : sortedById.entrySet()) {
sortedJsonObject.addProperty(entry.getValue(), entry.getKey());
}
if (sortedJsonObject.asMap().isEmpty()) {
if (Files.exists(this.cacheFilePath)) {
Files.delete(this.cacheFilePath);
}
} else {
FileUtils.createDirectoriesSafe(this.cacheFilePath.getParent());
GsonHelper.writeJsonFile(sortedJsonObject, this.cacheFilePath);
}
}
public static class IdConflictException extends RuntimeException {
private final String previousOwner;
private final int id;
public IdConflictException(String previousOwner, int id) {
super("ID " + id + " is already occupied by: " + previousOwner);
this.previousOwner = previousOwner;
this.id = id;
}
public String previousOwner() {
return previousOwner;
}
public int id() {
return id;
}
}
public static class IdExhaustedException extends RuntimeException {
private final String name;
private final int min;
private final int max;
public IdExhaustedException(String name, int min, int max) {
super("No available auto ID for '" + name + "'. All IDs in range " + min + "-" + max + " are occupied.");
this.name = name;
this.min = min;
this.max = max;
}
public String name() {
return name;
}
public int min() {
return min;
}
public int max() {
return max;
}
}
}

View File

@@ -0,0 +1,183 @@
package net.momirealms.craftengine.core.pack.allocator;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import net.momirealms.craftengine.core.block.AutoStateGroup;
import net.momirealms.craftengine.core.block.BlockStateWrapper;
import net.momirealms.craftengine.core.util.FileUtils;
import net.momirealms.craftengine.core.util.GsonHelper;
import net.momirealms.craftengine.core.util.Pair;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.function.Predicate;
public class VisualBlockStateAllocator {
private final Path cacheFilePath;
private final Map<String, BlockStateWrapper> cachedBlockStates = new HashMap<>();
private final Map<String, Pair<AutoStateGroup, CompletableFuture<BlockStateWrapper>>> pendingAllocations = new LinkedHashMap<>();
@SuppressWarnings("unchecked")
private final List<Pair<String, CompletableFuture<BlockStateWrapper>>>[] pendingAllocationFutures = new List[AutoStateGroup.values().length];
private final BlockStateCandidate[] candidates;
private final Function<String, BlockStateWrapper> factory;
private final Set<BlockStateWrapper> forcedStates = new HashSet<>();
public VisualBlockStateAllocator(Path cacheFilePath, BlockStateCandidate[] candidates, Function<String, BlockStateWrapper> factory) {
this.cacheFilePath = cacheFilePath;
this.candidates = candidates;
this.factory = factory;
}
public void reset() {
for (int i = 0; i < this.pendingAllocationFutures.length; i++) {
this.pendingAllocationFutures[i] = new ArrayList<>();
}
this.cachedBlockStates.clear();
this.pendingAllocations.clear();
this.forcedStates.clear();
}
public boolean isForcedState(final BlockStateWrapper state) {
return this.forcedStates.contains(state);
}
public CompletableFuture<BlockStateWrapper> assignFixedBlockState(String name, BlockStateWrapper state) {
this.cachedBlockStates.remove(name);
this.forcedStates.add(state);
BlockStateCandidate candidate = this.candidates[state.registryId()];
if (candidate != null) {
candidate.setUsed();
}
return CompletableFuture.completedFuture(state);
}
public CompletableFuture<BlockStateWrapper> requestAutoState(String name, AutoStateGroup group) {
CompletableFuture<BlockStateWrapper> future = new CompletableFuture<>();
this.pendingAllocations.put(name, new Pair<>(group, future));
this.pendingAllocationFutures[group.ordinal()].add(Pair.of(name, future));
return future;
}
public List<String> cleanupUnusedIds(Predicate<BlockStateWrapper> shouldRemove) {
List<String> idsToRemove = new ArrayList<>();
for (Map.Entry<String, BlockStateWrapper> entry : this.cachedBlockStates.entrySet()) {
if (shouldRemove.test(entry.getValue())) {
idsToRemove.add(entry.getKey());
}
}
for (String id : idsToRemove) {
this.cachedBlockStates.remove(id);
}
return idsToRemove;
}
public void processPendingAllocations() {
// 先处理缓存的
for (Map.Entry<String, BlockStateWrapper> entry : this.cachedBlockStates.entrySet()) {
int registryId = entry.getValue().registryId();
// 检查候选方块是否可用
BlockStateCandidate candidate = this.candidates[registryId];
if (candidate != null) {
// 未被使用
if (!candidate.isUsed()) {
// 获取当前的安排任务
Pair<AutoStateGroup, CompletableFuture<BlockStateWrapper>> pair = this.pendingAllocations.get(entry.getKey());
if (pair != null) {
// 如果候选满足组,那么直接允许起飞
if (pair.left().test(candidate.blockState())) {
pair.right().complete(candidate.blockState());
candidate.setUsed();
} else {
// 不满足候选组要求,那就等着分配新的吧
}
} else {
// 尽管未被使用,该槽位也应该被占用,以避免被自动分配到
candidate.setUsed();
}
}
// 被使用了就随他去
}
// 没有候选也随他去
}
this.pendingAllocations.clear();
for (AutoStateGroup group : AutoStateGroup.values()) {
List<Pair<String, CompletableFuture<BlockStateWrapper>>> pendingAllocationFuture = this.pendingAllocationFutures[group.ordinal()];
for (Pair<String, CompletableFuture<BlockStateWrapper>> pair : pendingAllocationFuture) {
if (!pair.right().isDone()) {
BlockStateCandidate nextCandidate = group.findNextCandidate();
if (nextCandidate != null) {
nextCandidate.setUsed();
this.cachedBlockStates.put(pair.left(), nextCandidate.blockState());
pair.right().complete(nextCandidate.blockState());
} else {
pair.right().completeExceptionally(new StateExhaustedException(group));
}
}
}
}
}
public static class StateExhaustedException extends RuntimeException {
private final AutoStateGroup group;
public StateExhaustedException(AutoStateGroup group) {
this.group = group;
}
public AutoStateGroup group() {
return group;
}
}
/**
* 从文件加载缓存
*/
public void loadFromCache() throws IOException {
if (!Files.exists(this.cacheFilePath)) {
return;
}
JsonElement element = GsonHelper.readJsonFile(this.cacheFilePath);
if (element instanceof JsonObject jsonObject) {
for (Map.Entry<String, JsonElement> entry : jsonObject.entrySet()) {
if (entry.getValue() instanceof JsonPrimitive primitive) {
String id = primitive.getAsString();
BlockStateWrapper state = this.factory.apply(id);
if (state != null) {
this.cachedBlockStates.put(entry.getKey(), state);
}
}
}
}
}
/**
* 保存缓存到文件
*/
public void saveToCache() throws IOException {
// 创建按ID排序的TreeMap
Map<BlockStateWrapper, String> sortedById = new TreeMap<>();
for (Map.Entry<String, BlockStateWrapper> entry : this.cachedBlockStates.entrySet()) {
sortedById.put(entry.getValue(), entry.getKey());
}
// 创建有序的JSON对象
JsonObject sortedJsonObject = new JsonObject();
for (Map.Entry<BlockStateWrapper, String> entry : sortedById.entrySet()) {
sortedJsonObject.addProperty(entry.getValue(), entry.getKey().getAsString());
}
if (sortedJsonObject.asMap().isEmpty()) {
if (Files.exists(this.cacheFilePath)) {
Files.delete(this.cacheFilePath);
}
} else {
FileUtils.createDirectoriesSafe(this.cacheFilePath.getParent());
GsonHelper.writeJsonFile(sortedJsonObject, this.cacheFilePath);
}
}
}

View File

@@ -0,0 +1,62 @@
package net.momirealms.craftengine.core.pack.allocator.cache;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
public class AllocationCacheFile<T extends Comparable<T>, A> {
private final Map<String, T> cache;
private final CacheStorage<A> cacheStorage;
private final CacheSerializer<T, A> serializer;
public AllocationCacheFile(CacheStorage<A> cacheStorage, CacheSerializer<T, A> serializer) {
this.cache = new HashMap<>();
this.cacheStorage = cacheStorage;
this.serializer = serializer;
}
public Map<String, T> cache() {
return this.cache;
}
public void clear() {
this.cache.clear();
}
public CompletableFuture<Void> load() {
return this.cacheStorage.load().thenAccept(a -> {
Map<String, T> deserialized = this.serializer.deserialize(a);
this.cache.putAll(deserialized);
});
}
public CompletableFuture<Void> save() {
Map<T, String> sortedById = new TreeMap<>();
for (Map.Entry<String, T> entry : this.cache.entrySet()) {
sortedById.put(entry.getValue(), entry.getKey());
}
return this.cacheStorage.save(this.serializer.serialize(sortedById));
}
public Iterable<Map.Entry<String, T>> entrySet() {
return this.cache.entrySet();
}
public Set<String> keySet() {
return this.cache.keySet();
}
public void put(String name, T newId) {
this.cache.put(name, newId);
}
public T remove(String name) {
return this.cache.remove(name);
}
public T get(String name) {
return this.cache.get(name);
}
}

View File

@@ -0,0 +1,98 @@
package net.momirealms.craftengine.core.pack.allocator.cache;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.CompletableFuture;
public interface CacheFileStorage<A> {
CompletableFuture<A> load();
CompletableFuture<Void> save(A value);
boolean needForceUpdate();
static <A> LocalFileCacheStorage<A> local(Path path, CacheFileType<A> type) {
return new LocalFileCacheStorage<>(path, type);
}
abstract class AbstractRemoteFileCacheStorage<A> implements CacheFileStorage<A> {
@Override
public boolean needForceUpdate() {
return true;
}
}
class LocalFileCacheStorage<A> implements CacheFileStorage<A> {
private final CacheFileType<A> fileType;
private final Path filePath;
private long lastModified = 0L;
public LocalFileCacheStorage(Path filePath, CacheFileType<A> type) {
this.filePath = filePath;
this.fileType = type;
updateLastModified();
}
@Override
public boolean needForceUpdate() {
try {
if (!Files.exists(this.filePath)) {
return this.lastModified != 0L; // 文件被删除了,需要更新
}
long currentModified = Files.getLastModifiedTime(this.filePath).toMillis();
if (currentModified > this.lastModified) {
this.lastModified = currentModified;
return true; // 文件被修改了,需要强制更新
}
return false;
} catch (IOException e) {
// 如果无法读取文件信息,保守起见返回 true 强制更新
return true;
}
}
@Override
public CompletableFuture<A> load() {
if (!Files.exists(this.filePath)) {
this.lastModified = 0L; // 重置最后修改时间
return CompletableFuture.completedFuture(this.fileType.create());
}
try {
A result = this.fileType.read(this.filePath);
updateLastModified(); // 加载成功后更新最后修改时间
return CompletableFuture.completedFuture(result);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
@Override
public CompletableFuture<Void> save(A value) {
try {
this.fileType.write(this.filePath, value);
updateLastModified(); // 保存成功后更新最后修改时间
return CompletableFuture.completedFuture(null);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
/**
* 更新最后修改时间
*/
private void updateLastModified() {
try {
if (Files.exists(this.filePath)) {
this.lastModified = Files.getLastModifiedTime(filePath).toMillis();
} else {
this.lastModified = 0L;
}
} catch (IOException e) {
this.lastModified = 0L; // 出错时重置
}
}
}
}

View File

@@ -0,0 +1,39 @@
package net.momirealms.craftengine.core.pack.allocator.cache;
import com.google.gson.JsonObject;
import net.momirealms.craftengine.core.util.GsonHelper;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public interface CacheFileType<T> {
JsonCacheFileType JSON = new JsonCacheFileType();
T read(Path path) throws IOException;
void write(Path path, T value) throws IOException;
T create();
class JsonCacheFileType implements CacheFileType<JsonObject> {
@Override
public JsonObject read(Path path) throws IOException {
if (Files.exists(path)) {
return GsonHelper.readJsonFile(path).getAsJsonObject();
}
return new JsonObject();
}
@Override
public void write(Path path, JsonObject value) throws IOException {
GsonHelper.writeJsonFile(value, path);
}
@Override
public JsonObject create() {
return new JsonObject();
}
}
}

View File

@@ -0,0 +1,51 @@
package net.momirealms.craftengine.core.pack.allocator.cache;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import java.util.HashMap;
import java.util.Map;
public interface CacheSerializer<T extends Comparable<T>, A> {
A serialize(Map<T, String> obj);
Map<String, T> deserialize(A obj);
static <T extends Comparable<T>> CacheSerializer<T, JsonObject> json() {
return new JsonSerializer<>();
}
class JsonSerializer<T extends Comparable<T>> implements CacheSerializer<T, JsonObject> {
@Override
public JsonObject serialize(Map<T, String> obj) {
JsonObject jsonObject = new JsonObject();
for (Map.Entry<T, String> entry : obj.entrySet()) {
if (entry.getKey() instanceof Integer i) {
jsonObject.addProperty(entry.getValue(), i);
} else if (entry.getKey() instanceof String s) {
jsonObject.addProperty(entry.getValue(), s);
}
}
return jsonObject;
}
@SuppressWarnings("unchecked")
@Override
public Map<String, T> deserialize(JsonObject obj) {
Map<String, T> map = new HashMap<>();
for (Map.Entry<String, JsonElement> entry : obj.entrySet()) {
if (entry.getValue() instanceof JsonPrimitive primitive) {
if (primitive.isNumber()) {
map.put(entry.getKey(), (T) (Integer) primitive.getAsInt());
} else if (primitive.isString()) {
map.put(entry.getKey(), (T) primitive.getAsString());
}
}
}
return map;
}
}
}

View File

@@ -0,0 +1,32 @@
package net.momirealms.craftengine.core.pack.allocator.cache;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
public class CacheStorage<A> {
private final CacheFileStorage<A> storage;
private A lastReadValue;
public CacheStorage(CacheFileStorage<A> storage) {
this.storage = storage;
}
public CompletableFuture<Void> save(@NotNull final A value) {
if (!value.equals(this.lastReadValue) || this.storage.needForceUpdate()) {
this.lastReadValue = value;
return this.storage.save(value);
}
return CompletableFuture.completedFuture(null);
}
public CompletableFuture<A> load() {
if (this.lastReadValue != null && !this.storage.needForceUpdate()) {
return CompletableFuture.completedFuture(this.lastReadValue);
}
return this.storage.load().thenApply(a -> {
this.lastReadValue = a;
return a;
});
}
}

View File

@@ -19,63 +19,148 @@ public class ResolutionMergePackMcMeta implements Resolution {
this.description = description;
}
private static class MinMax {
int min;
int max;
MinMax(int min, int max) {
this.min = min;
this.max = max;
}
record MinMax(int min, int max) {
}
@SuppressWarnings("DuplicatedCode")
public static void mergeMcMeta(Path file1, Path file2, JsonElement customDescription) throws IOException {
JsonElement elem1 = GsonHelper.readJsonFile(file1);
JsonElement elem2 = GsonHelper.readJsonFile(file2);
JsonObject mcmeta1 = GsonHelper.readJsonFile(file1).getAsJsonObject();
JsonObject mcmeta2 = GsonHelper.readJsonFile(file2).getAsJsonObject();
JsonObject merged = mergeValues(elem1.getAsJsonObject(), elem2.getAsJsonObject())
.getAsJsonObject();
JsonObject merged = mergeValues(mcmeta1, mcmeta2).getAsJsonObject();
if (merged.has("pack")) {
JsonObject pack = merged.getAsJsonObject("pack");
if (mcmeta1.has("pack") && mcmeta2.has("pack")) {
JsonObject mergedPack = merged.getAsJsonObject("pack");
JsonObject mcmeta1Pack = mcmeta1.getAsJsonObject("pack");
JsonObject mcmeta2Pack = mcmeta2.getAsJsonObject("pack");
int pf1 = elem1.getAsJsonObject().getAsJsonObject("pack")
.getAsJsonPrimitive("pack_format").getAsInt();
int pf2 = elem2.getAsJsonObject().getAsJsonObject("pack")
.getAsJsonPrimitive("pack_format").getAsInt();
pack.addProperty("pack_format", Math.max(pf1, pf2));
int minPackFormat = Integer.MAX_VALUE;
int maxPackFormat = Integer.MIN_VALUE;
JsonArray mergedMinFormat = null;
JsonArray mergedMaxFormat = null;
JsonElement sf1 = elem1.getAsJsonObject().getAsJsonObject("pack")
.get("supported_formats");
JsonElement sf2 = elem2.getAsJsonObject().getAsJsonObject("pack")
.get("supported_formats");
if (mcmeta1Pack.has("pack_format") && mcmeta2Pack.has("pack_format")) {
int packFormat1 = mcmeta1Pack.getAsJsonPrimitive("pack_format").getAsInt();
int packFormat2 = mcmeta2Pack.getAsJsonPrimitive("pack_format").getAsInt();
int mergedPackFormat = maxPackFormat = Math.max(packFormat1, packFormat2);
minPackFormat = Math.min(packFormat1, packFormat2);
mergedPack.addProperty("pack_format", mergedPackFormat);
} else if (mcmeta1Pack.has("pack_format")) {
minPackFormat = maxPackFormat = mcmeta1Pack.getAsJsonPrimitive("pack_format").getAsInt();
} else if (mcmeta2Pack.has("pack_format")) {
minPackFormat = maxPackFormat = mcmeta2Pack.getAsJsonPrimitive("pack_format").getAsInt();
}
if (sf1 != null || sf2 != null) {
MinMax mergedMinMax = getMergedMinMax(sf1, sf2, pf1, pf2);
if (mcmeta1Pack.has("min_format") || mcmeta2Pack.has("min_format")) {
int[] minFormat1 = new int[]{Integer.MAX_VALUE, 0};
int[] minFormat2 = new int[]{Integer.MAX_VALUE, 0};
JsonElement mergedSf = createSupportedFormatsElement(
sf1 != null ? sf1 : sf2,
if (mcmeta1Pack.has("min_format")) {
JsonElement minFormat = mcmeta1Pack.get("min_format");
if (minFormat.isJsonPrimitive()) {
minFormat1[0] = minFormat.getAsInt();
}
if (minFormat.isJsonArray()) {
JsonArray minFormatArray = minFormat.getAsJsonArray();
minFormat1[0] = minFormatArray.get(0).getAsInt();
if (minFormatArray.size() > 1) {
minFormat1[1] = minFormatArray.get(1).getAsInt();
}
}
}
if (mcmeta2Pack.has("min_format")) {
JsonElement minFormat = mcmeta2Pack.get("min_format");
if (minFormat.isJsonPrimitive()) {
minFormat2[0] = minFormat.getAsInt();
}
if (mcmeta2Pack.isJsonArray()) {
JsonArray minFormatArray = minFormat.getAsJsonArray();
minFormat2[0] = minFormatArray.get(0).getAsInt();
if (minFormatArray.size() > 1) {
minFormat2[1] = minFormatArray.get(1).getAsInt();
}
}
}
minPackFormat = Math.min(minPackFormat, Math.min(minFormat1[0], minFormat2[0]));
mergedMinFormat = new JsonArray(2);
mergedMinFormat.add(minPackFormat);
mergedMinFormat.add(Math.min(minFormat1[1], minFormat2[1]));
mergedPack.add("min_format", mergedMinFormat);
}
if (mcmeta1Pack.has("max_format") || mcmeta2Pack.has("max_format")) {
int[] maxFormat1 = new int[]{Integer.MIN_VALUE, 0};
int[] maxFormat2 = new int[]{Integer.MIN_VALUE, 0};
if (mcmeta1Pack.has("max_format")) {
JsonElement maxFormat = mcmeta1Pack.get("max_format");
if (maxFormat.isJsonPrimitive()) {
maxFormat1[0] = maxFormat.getAsInt();
}
if (maxFormat.isJsonArray()) {
JsonArray maxFormatArray = maxFormat.getAsJsonArray();
maxFormat1[0] = maxFormatArray.get(0).getAsInt();
if (maxFormatArray.size() > 1) {
maxFormat1[1] = maxFormatArray.get(1).getAsInt();
}
}
}
if (mcmeta2Pack.has("max_format")) {
JsonElement maxFormat = mcmeta2Pack.get("max_format");
if (maxFormat.isJsonPrimitive()) {
maxFormat2[0] = maxFormat.getAsInt();
}
if (maxFormat.isJsonArray()) {
JsonArray maxFormatArray = maxFormat.getAsJsonArray();
maxFormat2[0] = maxFormatArray.get(0).getAsInt();
if (maxFormatArray.size() > 1) {
maxFormat2[1] = maxFormatArray.get(1).getAsInt();
}
}
}
maxPackFormat = Math.max(maxPackFormat, Math.max(maxFormat1[0], maxFormat2[0]));
mergedMaxFormat = new JsonArray(2);
mergedMaxFormat.add(maxPackFormat);
mergedMaxFormat.add(Math.max(maxFormat1[1], maxFormat2[1]));
mergedPack.add("max_format", mergedMaxFormat);
}
JsonElement supportedFormats1 = mcmeta1Pack.get("supported_formats");
JsonElement supportedFormats2 = mcmeta2Pack.get("supported_formats");
if (supportedFormats1 != null || supportedFormats2 != null) {
MinMax mergedMinMax = getMergedMinMax(supportedFormats1, supportedFormats2, minPackFormat, maxPackFormat);
JsonElement mergedSupportedFormats = createSupportedFormatsElement(
supportedFormats1 != null ? supportedFormats1 : supportedFormats2,
mergedMinMax.min,
mergedMinMax.max
);
pack.add("supported_formats", mergedSf);
if (mergedMinFormat != null && !mergedMinFormat.isEmpty()) {
mergedMinFormat.set(0, new JsonPrimitive(Math.min(mergedMinMax.min, mergedMinFormat.get(0).getAsInt())));
}
if (mergedMaxFormat != null && !mergedMaxFormat.isEmpty()) {
mergedMaxFormat.set(0, new JsonPrimitive(Math.max(mergedMinMax.max, mergedMaxFormat.get(0).getAsInt())));
}
mergedPack.add("supported_formats", mergedSupportedFormats);
}
if (customDescription != null) {
pack.add("description", customDescription);
mergedPack.add("description", customDescription);
} else {
JsonPrimitive desc1 = elem1.getAsJsonObject().getAsJsonObject("pack")
JsonPrimitive description1 = mcmeta1.getAsJsonObject().getAsJsonObject("pack")
.getAsJsonPrimitive("description");
JsonPrimitive desc2 = elem2.getAsJsonObject().getAsJsonObject("pack")
JsonPrimitive description2 = mcmeta2.getAsJsonObject().getAsJsonObject("pack")
.getAsJsonPrimitive("description");
String mergedDesc = (desc1 != null ? desc1.getAsString() : "")
+ (desc1 != null && desc2 != null ? "\n" : "")
+ (desc2 != null ? desc2.getAsString() : "");
String mergedDesc = (description1 != null ? description1.getAsString() : "")
+ (description1 != null && description2 != null ? "\n" : "")
+ (description2 != null ? description2.getAsString() : "");
if (!mergedDesc.isEmpty()) {
pack.addProperty("description", mergedDesc);
mergedPack.addProperty("description", mergedDesc);
}
}
}
@@ -83,16 +168,14 @@ public class ResolutionMergePackMcMeta implements Resolution {
GsonHelper.writeJsonFile(merged, file1);
}
private static MinMax getMergedMinMax(JsonElement sf1, JsonElement sf2, int pf1, int pf2) {
private static MinMax getMergedMinMax(JsonElement sf1, JsonElement sf2, int minPackFormat, int maxPackFormat) {
MinMax mm1 = parseSupportedFormats(sf1);
MinMax mm2 = parseSupportedFormats(sf2);
int finalMin = Math.min(mm1.min, mm2.min);
int finalMax = Math.max(mm1.max, mm2.max);
int pfMin = Math.min(pf1, pf2);
int pfMax = Math.max(pf1, pf2);
finalMin = Math.min(pfMin, finalMin);
finalMax = Math.max(pfMax, finalMax);
finalMin = Math.min(minPackFormat, finalMin);
finalMax = Math.max(maxPackFormat, finalMax);
return new MinMax(finalMin, finalMax);
}
@@ -103,14 +186,27 @@ public class ResolutionMergePackMcMeta implements Resolution {
}
if (supported.isJsonPrimitive()) {
int value = supported.getAsInt();
return new MinMax(value, value);
if (supported.getAsJsonPrimitive().isNumber()) {
int value = supported.getAsInt();
return new MinMax(value, value);
} else if (supported.getAsJsonPrimitive().isString()) {
String value = supported.getAsString();
if (value.contains(",")) {
String[] parts = value.replace("[", "").replace("]", "").split(",");
int min = Integer.parseInt(parts[0]);
int max = Integer.parseInt(parts[1]);
return new MinMax(min, max);
} else {
int min = Integer.parseInt(value);
return new MinMax(min, min);
}
}
}
if (supported.isJsonArray()) {
JsonArray arr = supported.getAsJsonArray();
int min = arr.get(0).getAsInt();
int max = arr.get(arr.size()-1).getAsInt();
int max = arr.get(arr.size() - 1).getAsInt();
return new MinMax(min, max);
}

View File

@@ -92,6 +92,8 @@ public abstract class CraftEngine implements Plugin {
protected CraftEngine(Consumer<CraftEngine> reloadEventDispatcher) {
instance = this;
this.reloadEventDispatcher = reloadEventDispatcher;
((Logger) LogManager.getRootLogger()).addFilter(new LogFilter());
((Logger) LogManager.getRootLogger()).addFilter(new DisconnectLogFilter());
}
public static CraftEngine instance() {
@@ -105,9 +107,6 @@ public abstract class CraftEngine implements Plugin {
RecipeDisplayTypes.init();
SlotDisplayTypes.init();
LegacyRecipeTypes.init();
((Logger) LogManager.getRootLogger()).addFilter(new LogFilter());
((Logger) LogManager.getRootLogger()).addFilter(new DisconnectLogFilter());
this.config.load();
}
public record ReloadResult(boolean success, long asyncTime, long syncTime) {
@@ -173,6 +172,7 @@ public abstract class CraftEngine implements Plugin {
// collect illegal characters from minecraft:default font
this.fontManager.delayedLoad();
this.advancementManager.delayedLoad();
this.soundManager.delayedLoad();
if (reloadRecipe) {
// convert data pack recipes
this.recipeManager.delayedLoad();
@@ -281,7 +281,7 @@ public abstract class CraftEngine implements Plugin {
// register furniture parser
this.packManager.registerConfigSectionParser(this.furnitureManager.parser());
// register block parser
this.packManager.registerConfigSectionParser(this.blockManager.parser());
this.packManager.registerConfigSectionParsers(this.blockManager.parsers());
// register recipe parser
this.packManager.registerConfigSectionParser(this.recipeManager.parser());
// register category parser

View File

@@ -0,0 +1,32 @@
package net.momirealms.craftengine.core.plugin.config;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import net.momirealms.craftengine.core.pack.CachedConfigSection;
public abstract class AbstractConfigParser implements ConfigParser {
protected final ObjectArrayList<CachedConfigSection> configStorage;
public AbstractConfigParser() {
this.configStorage = new ObjectArrayList<>();
}
@Override
public void addConfig(CachedConfigSection section) {
this.configStorage.add(section);
}
@Override
public void loadAll() {
Object[] elements = this.configStorage.elements();
for (int i = 0, size = this.configStorage.size(); i < size; i++) {
parseSection((CachedConfigSection) elements[i]);
}
}
@Override
public void clear() {
this.configStorage.clear();
}
protected abstract void parseSection(CachedConfigSection section);
}

View File

@@ -44,6 +44,7 @@ public class Config {
protected boolean checkUpdate;
protected boolean metrics;
protected boolean filterConfigurationPhaseDisconnect;
protected Locale forcedLocale;
protected boolean debug$common;
protected boolean debug$packet;
@@ -63,7 +64,8 @@ public class Config {
protected boolean resource_pack$protection$crash_tools$method_2;
protected boolean resource_pack$protection$crash_tools$method_3;
protected boolean resource_pack$validate$enable;
protected boolean resource_pack$validation$enable;
protected boolean resource_pack$validation$fix_atlas;
protected boolean resource_pack$exclude_core_shaders;
protected boolean resource_pack$protection$obfuscation$enable;
@@ -123,6 +125,7 @@ public class Config {
protected int block$predict_breaking_interval;
protected double block$extended_interaction_range;
protected boolean block$chunk_relighter;
protected int block$serverside_blocks = -1;
protected boolean recipe$enable;
protected boolean recipe$disable_vanilla_recipes$all;
@@ -134,21 +137,24 @@ public class Config {
protected boolean image$illegal_characters_filter$anvil;
protected boolean image$illegal_characters_filter$sign;
protected boolean image$illegal_characters_filter$book;
protected boolean image$intercept_packets$system_chat;
protected boolean image$intercept_packets$tab_list;
protected boolean image$intercept_packets$actionbar;
protected boolean image$intercept_packets$title;
protected boolean image$intercept_packets$bossbar;
protected boolean image$intercept_packets$container;
protected boolean image$intercept_packets$team;
protected boolean image$intercept_packets$scoreboard;
protected boolean image$intercept_packets$entity_name;
protected boolean image$intercept_packets$text_display;
protected boolean image$intercept_packets$armor_stand;
protected boolean image$intercept_packets$player_info;
protected boolean image$intercept_packets$set_score;
protected boolean image$intercept_packets$item;
protected boolean image$intercept_packets$advancement;
protected int image$codepoint_starting_value$default;
protected Map<Key, Integer> image$codepoint_starting_value$overrides;
protected boolean network$intercept_packets$system_chat;
protected boolean network$intercept_packets$tab_list;
protected boolean network$intercept_packets$actionbar;
protected boolean network$intercept_packets$title;
protected boolean network$intercept_packets$bossbar;
protected boolean network$intercept_packets$container;
protected boolean network$intercept_packets$team;
protected boolean network$intercept_packets$scoreboard;
protected boolean network$intercept_packets$entity_name;
protected boolean network$intercept_packets$text_display;
protected boolean network$intercept_packets$armor_stand;
protected boolean network$intercept_packets$player_info;
protected boolean network$intercept_packets$set_score;
protected boolean network$intercept_packets$item;
protected boolean network$intercept_packets$advancement;
protected boolean item$client_bound_model;
protected boolean item$non_italic_tag;
@@ -156,6 +162,9 @@ public class Config {
protected boolean item$update_triggers$click_in_inventory;
protected boolean item$update_triggers$drop;
protected boolean item$update_triggers$pick_up;
protected int item$custom_model_data_starting_value$default;
protected Map<Key, Integer> item$custom_model_data_starting_value$overrides;
protected boolean item$always_use_item_model;
protected String equipment$sacrificed_vanilla_armor$type;
protected Key equipment$sacrificed_vanilla_armor$asset_id;
@@ -175,7 +184,7 @@ public class Config {
instance = this;
}
public void load() {
public boolean updateConfigCache() {
// 文件不存在,则保存
if (!Files.exists(this.configFilePath)) {
this.plugin.saveResource("config.yml");
@@ -193,13 +202,20 @@ public class Config {
this.updateConfigVersion(configFileBytes);
}
}
// 加载配置文件
this.loadSettings();
this.lastModified = lastModified;
this.size = size;
return true;
}
} catch (IOException e) {
this.plugin.logger().severe("Failed to load config.yml", e);
this.plugin.logger().severe("Failed to update config.yml", e);
}
return false;
}
public void load() {
boolean isUpdated = updateConfigCache();
if (isUpdated) {
loadFullSettings();
}
}
@@ -229,6 +245,7 @@ public class Config {
.addIgnoredRoute(PluginProperties.getValue("config"), "resource-pack.delivery.hosting", '.')
.addIgnoredRoute(PluginProperties.getValue("config"), "chunk-system.process-invalid-blocks.convert", '.')
.addIgnoredRoute(PluginProperties.getValue("config"), "chunk-system.process-invalid-furniture.convert", '.')
.addIgnoredRoute(PluginProperties.getValue("config"), "item.custom-model-data-starting-value.overrides", '.')
.build());
}
try {
@@ -238,9 +255,15 @@ public class Config {
}
}
private void loadSettings() {
public void loadForcedLocale() {
YamlDocument config = settings();
plugin.translationManager().forcedLocale(TranslationManager.parseLocale(config.getString("forced-locale", "")));
forcedLocale = TranslationManager.parseLocale(config.getString("forced-locale", ""));
}
@SuppressWarnings("DuplicatedCode")
public void loadFullSettings() {
YamlDocument config = settings();
forcedLocale = TranslationManager.parseLocale(config.getString("forced-locale", ""));
// basics
metrics = config.getBoolean("metrics", false);
@@ -278,7 +301,7 @@ public class Config {
resource_pack$protection$crash_tools$method_1 = config.getBoolean("resource-pack.protection.crash-tools.method-1", false);
resource_pack$protection$crash_tools$method_2 = config.getBoolean("resource-pack.protection.crash-tools.method-2", false);
resource_pack$protection$crash_tools$method_3 = config.getBoolean("resource-pack.protection.crash-tools.method-3", false);
resource_pack$protection$obfuscation$enable = config.getBoolean("resource-pack.protection.obfuscation.enable", false);
resource_pack$protection$obfuscation$enable = VersionHelper.PREMIUM && config.getBoolean("resource-pack.protection.obfuscation.enable", false);
resource_pack$protection$obfuscation$seed = config.getLong("resource-pack.protection.obfuscation.seed", 0L);
resource_pack$protection$obfuscation$fake_directory = config.getBoolean("resource-pack.protection.obfuscation.fake-directory", false);
resource_pack$protection$obfuscation$escape_unicode = config.getBoolean("resource-pack.protection.obfuscation.escape-unicode", false);
@@ -295,7 +318,8 @@ public class Config {
resource_pack$protection$obfuscation$resource_location$bypass_models = config.getStringList("resource-pack.protection.obfuscation.resource-location.bypass-models");
resource_pack$protection$obfuscation$resource_location$bypass_sounds = config.getStringList("resource-pack.protection.obfuscation.resource-location.bypass-sounds");
resource_pack$protection$obfuscation$resource_location$bypass_equipments = config.getStringList("resource-pack.protection.obfuscation.resource-location.bypass-equipments");
resource_pack$validate$enable = config.getBoolean("resource-pack.validate.enable", true);
resource_pack$validation$enable = config.getBoolean("resource-pack.validation.enable", true);
resource_pack$validation$fix_atlas = VersionHelper.PREMIUM && config.getBoolean("resource-pack.validation.fix-atlas", true);
resource_pack$exclude_core_shaders = config.getBoolean("resource-pack.exclude-core-shaders", false);
resource_pack$overlay_format = config.getString("resource-pack.overlay-format", "overlay_{version}");
if (!resource_pack$overlay_format.contains("{version}")) {
@@ -377,12 +401,29 @@ public class Config {
equipment$sacrificed_vanilla_armor$humanoid_leggings = Key.of(config.getString("equipment.sacrificed-vanilla-armor.humanoid-leggings", "minecraft:trims/entity/humanoid_leggings/chainmail"));
// item
item$client_bound_model = config.getBoolean("item.client-bound-model", false);
item$client_bound_model = config.getBoolean("item.client-bound-model", true) && VersionHelper.PREMIUM;
item$non_italic_tag = config.getBoolean("item.non-italic-tag", false);
item$update_triggers$attack = config.getBoolean("item.update-triggers.attack", false);
item$update_triggers$click_in_inventory = config.getBoolean("item.update-triggers.click-in-inventory", false);
item$update_triggers$drop = config.getBoolean("item.update-triggers.drop", false);
item$update_triggers$pick_up = config.getBoolean("item.update-triggers.pick-up", false);
item$custom_model_data_starting_value$default = config.getInt("item.custom-model-data-starting-value.default", 10000);
item$always_use_item_model = config.getBoolean("item.always-use-item-model", true) && VersionHelper.isOrAbove1_21_2();
Section customModelDataOverridesSection = config.getSection("item.custom-model-data-starting-value.overrides");
if (customModelDataOverridesSection != null) {
Map<Key, Integer> customModelDataOverrides = new HashMap<>();
for (Map.Entry<String, Object> entry : customModelDataOverridesSection.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof String s) {
customModelDataOverrides.put(Key.of(entry.getKey()), Integer.parseInt(s));
} else if (entry.getValue() instanceof Integer i) {
customModelDataOverrides.put(Key.of(entry.getKey()), i);
}
}
item$custom_model_data_starting_value$overrides = customModelDataOverrides;
} else {
item$custom_model_data_starting_value$overrides = Map.of();
}
// block
block$sound_system$enable = config.getBoolean("block.sound-system.enable", true);
@@ -392,6 +433,10 @@ public class Config {
block$predict_breaking_interval = Math.max(config.getInt("block.predict-breaking.interval", 10), 1);
block$extended_interaction_range = Math.max(config.getDouble("block.predict-breaking.extended-interaction-range", 0.5), 0.0);
block$chunk_relighter = config.getBoolean("block.chunk-relighter", true);
if (firstTime) {
block$serverside_blocks = Math.min(config.getInt("block.serverside-blocks", 2000), 10_0000);
if (block$serverside_blocks < 0) block$serverside_blocks = 0;
}
// recipe
recipe$enable = config.getBoolean("recipe.enable", true);
@@ -405,21 +450,38 @@ public class Config {
image$illegal_characters_filter$chat = config.getBoolean("image.illegal-characters-filter.chat", true);
image$illegal_characters_filter$command = config.getBoolean("image.illegal-characters-filter.command", true);
image$illegal_characters_filter$sign = config.getBoolean("image.illegal-characters-filter.sign", true);
image$intercept_packets$system_chat = config.getBoolean("image.intercept-packets.system-chat", true);
image$intercept_packets$tab_list = config.getBoolean("image.intercept-packets.tab-list", true);
image$intercept_packets$actionbar = config.getBoolean("image.intercept-packets.actionbar", true);
image$intercept_packets$title = config.getBoolean("image.intercept-packets.title", true);
image$intercept_packets$bossbar = config.getBoolean("image.intercept-packets.bossbar", true);
image$intercept_packets$container = config.getBoolean("image.intercept-packets.container", true);
image$intercept_packets$team = config.getBoolean("image.intercept-packets.team", true);
image$intercept_packets$scoreboard = config.getBoolean("image.intercept-packets.scoreboard", true);
image$intercept_packets$entity_name = config.getBoolean("image.intercept-packets.entity-name", false);
image$intercept_packets$text_display = config.getBoolean("image.intercept-packets.text-display", true);
image$intercept_packets$armor_stand = config.getBoolean("image.intercept-packets.armor-stand", true);
image$intercept_packets$player_info = config.getBoolean("image.intercept-packets.player-info", true);
image$intercept_packets$set_score = config.getBoolean("image.intercept-packets.set-score", true);
image$intercept_packets$item = config.getBoolean("image.intercept-packets.item", true);
image$intercept_packets$advancement = config.getBoolean("image.intercept-packets.advancement", true);
image$codepoint_starting_value$default = config.getInt("image.codepoint-starting-value.default", 0);
Section codepointOverridesSection = config.getSection("image.codepoint-starting-value.overrides");
if (codepointOverridesSection != null) {
Map<Key, Integer> codepointOverrides = new HashMap<>();
for (Map.Entry<String, Object> entry : codepointOverridesSection.getStringRouteMappedValues(false).entrySet()) {
if (entry.getValue() instanceof String s) {
codepointOverrides.put(Key.of(entry.getKey()), Integer.parseInt(s));
} else if (entry.getValue() instanceof Integer i) {
codepointOverrides.put(Key.of(entry.getKey()), i);
}
}
image$codepoint_starting_value$overrides = codepointOverrides;
} else {
image$codepoint_starting_value$overrides = Map.of();
}
network$intercept_packets$system_chat = config.getBoolean("network.intercept-packets.system-chat", true);
network$intercept_packets$tab_list = config.getBoolean("network.intercept-packets.tab-list", true);
network$intercept_packets$actionbar = config.getBoolean("network.intercept-packets.actionbar", true);
network$intercept_packets$title = config.getBoolean("network.intercept-packets.title", true);
network$intercept_packets$bossbar = config.getBoolean("network.intercept-packets.bossbar", true);
network$intercept_packets$container = config.getBoolean("network.intercept-packets.container", true);
network$intercept_packets$team = config.getBoolean("network.intercept-packets.team", true);
network$intercept_packets$scoreboard = config.getBoolean("network.intercept-packets.scoreboard", true);
network$intercept_packets$entity_name = config.getBoolean("network.intercept-packets.entity-name", false);
network$intercept_packets$text_display = config.getBoolean("network.intercept-packets.text-display", true);
network$intercept_packets$armor_stand = config.getBoolean("network.intercept-packets.armor-stand", true);
network$intercept_packets$player_info = config.getBoolean("network.intercept-packets.player-info", true);
network$intercept_packets$set_score = config.getBoolean("network.intercept-packets.set-score", true);
network$intercept_packets$item = config.getBoolean("network.intercept-packets.item", true);
network$intercept_packets$advancement = config.getBoolean("network.intercept-packets.advancement", true);
// emoji
emoji$contexts$chat = config.getBoolean("emoji.contexts.chat", true);
@@ -441,6 +503,10 @@ public class Config {
return MinecraftVersion.parse(version);
}
public static Locale forcedLocale() {
return instance.forcedLocale;
}
public static String configVersion() {
return instance.configVersion;
}
@@ -461,6 +527,10 @@ public class Config {
return false;
}
public static boolean debugBlock() {
return false;
}
public static boolean debugFurniture() {
return instance.debug$furniture;
}
@@ -477,6 +547,14 @@ public class Config {
return instance.metrics;
}
public static int serverSideBlocks() {
return instance.block$serverside_blocks;
}
public static boolean alwaysUseItemModel() {
return instance.item$always_use_item_model;
}
public static boolean filterConfigurationPhaseDisconnect() {
return instance.filterConfigurationPhaseDisconnect;
}
@@ -720,6 +798,20 @@ public class Config {
return instance.furniture$hide_base_entity;
}
public static int customModelDataStartingValue(Key material) {
if (instance.item$custom_model_data_starting_value$overrides.containsKey(material)) {
return instance.item$custom_model_data_starting_value$overrides.get(material);
}
return instance.item$custom_model_data_starting_value$default;
}
public static int codepointStartingValue(Key font) {
if (instance.image$codepoint_starting_value$overrides.containsKey(font)) {
return instance.image$codepoint_starting_value$overrides.get(font);
}
return instance.image$codepoint_starting_value$default;
}
public static int compressionMethod() {
int id = instance.chunk_system$compression_method;
if (id <= 0 || id > CompressionMethod.METHOD_COUNT) {
@@ -729,63 +821,63 @@ public class Config {
}
public static boolean interceptSystemChat() {
return instance.image$intercept_packets$system_chat;
return instance.network$intercept_packets$system_chat;
}
public static boolean interceptTabList() {
return instance.image$intercept_packets$tab_list;
return instance.network$intercept_packets$tab_list;
}
public static boolean interceptActionBar() {
return instance.image$intercept_packets$actionbar;
return instance.network$intercept_packets$actionbar;
}
public static boolean interceptTitle() {
return instance.image$intercept_packets$title;
return instance.network$intercept_packets$title;
}
public static boolean interceptBossBar() {
return instance.image$intercept_packets$bossbar;
return instance.network$intercept_packets$bossbar;
}
public static boolean interceptContainer() {
return instance.image$intercept_packets$container;
return instance.network$intercept_packets$container;
}
public static boolean interceptTeam() {
return instance.image$intercept_packets$team;
return instance.network$intercept_packets$team;
}
public static boolean interceptEntityName() {
return instance.image$intercept_packets$entity_name;
return instance.network$intercept_packets$entity_name;
}
public static boolean interceptScoreboard() {
return instance.image$intercept_packets$scoreboard;
return instance.network$intercept_packets$scoreboard;
}
public static boolean interceptTextDisplay() {
return instance.image$intercept_packets$text_display;
return instance.network$intercept_packets$text_display;
}
public static boolean interceptArmorStand() {
return instance.image$intercept_packets$armor_stand;
return instance.network$intercept_packets$armor_stand;
}
public static boolean interceptPlayerInfo() {
return instance.image$intercept_packets$player_info;
return instance.network$intercept_packets$player_info;
}
public static boolean interceptSetScore() {
return instance.image$intercept_packets$set_score;
return instance.network$intercept_packets$set_score;
}
public static boolean interceptItem() {
return instance.image$intercept_packets$item;
return instance.network$intercept_packets$item;
}
public static boolean interceptAdvancement() {
return instance.image$intercept_packets$advancement;
return instance.network$intercept_packets$advancement;
}
public static boolean predictBreaking() {
@@ -837,7 +929,11 @@ public class Config {
}
public static boolean validateResourcePack() {
return instance.resource_pack$validate$enable;
return instance.resource_pack$validation$enable;
}
public static boolean fixTextureAtlas() {
return instance.resource_pack$validation$fix_atlas;
}
public static boolean excludeShaders() {

View File

@@ -1,30 +1,14 @@
package net.momirealms.craftengine.core.plugin.config;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.plugin.locale.LocalizedException;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.pack.CachedConfigSection;
import org.jetbrains.annotations.NotNull;
import java.nio.file.Path;
import java.util.Map;
public interface ConfigParser extends Comparable<ConfigParser> {
String[] sectionId();
default void parseSection(Pack pack, Path path, Key id, Map<String, Object> section) throws LocalizedException {
this.parseObject(pack, path, id, section);
}
default void parseObject(Pack pack, Path path, Key id, Object object) throws LocalizedException {
}
int loadingSequence();
default boolean supportsParsingObject() {
return false;
}
@Override
default int compareTo(@NotNull ConfigParser another) {
return Integer.compare(loadingSequence(), another.loadingSequence());
@@ -35,4 +19,10 @@ public interface ConfigParser extends Comparable<ConfigParser> {
default void preProcess() {
}
void addConfig(CachedConfigSection section);
void loadAll();
void clear();
}

View File

@@ -0,0 +1,31 @@
package net.momirealms.craftengine.core.plugin.config;
import net.momirealms.craftengine.core.pack.CachedConfigSection;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.plugin.locale.LocalizedException;
import net.momirealms.craftengine.core.util.GsonHelper;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.ResourceConfigUtils;
import java.nio.file.Path;
import java.util.Map;
public abstract class IdObjectConfigParser extends AbstractConfigParser {
@Override
protected void parseSection(CachedConfigSection cached) {
for (Map.Entry<String, Object> configEntry : cached.config().entrySet()) {
String key = configEntry.getKey();
Key id = Key.withDefaultNamespace(key, cached.pack().namespace());
String node = cached.prefix() + "." + key;
ResourceConfigUtils.runCatching(
cached.filePath(),
node,
() -> parseObject(cached.pack(), cached.filePath(), node, id, configEntry.getValue()),
() -> GsonHelper.get().toJson(configEntry.getValue())
);
}
}
protected abstract void parseObject(Pack pack, Path path, String node, Key id, Object object) throws LocalizedException;
}

View File

@@ -0,0 +1,48 @@
package net.momirealms.craftengine.core.plugin.config;
import net.momirealms.craftengine.core.pack.CachedConfigSection;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.plugin.locale.LocalizedException;
import net.momirealms.craftengine.core.plugin.locale.TranslationManager;
import net.momirealms.craftengine.core.util.GsonHelper;
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.util.Map;
import static net.momirealms.craftengine.core.util.MiscUtils.castToMap;
public abstract class IdSectionConfigParser extends AbstractConfigParser {
@Override
protected void parseSection(CachedConfigSection cached) {
for (Map.Entry<String, Object> configEntry : cached.config().entrySet()) {
String key = configEntry.getKey();
Key id = Key.withDefaultNamespace(key, cached.pack().namespace());
if (!(configEntry.getValue() instanceof Map<?, ?> section)) {
TranslationManager.instance().log("warning.config.structure.not_section",
cached.filePath().toString(), cached.prefix() + "." + key, configEntry.getValue().getClass().getSimpleName());
continue;
}
Map<String, Object> config = castToMap(section, false);
if ((boolean) config.getOrDefault("debug", false)) {
CraftEngine.instance().logger().info(GsonHelper.get().toJson(CraftEngine.instance().templateManager().applyTemplates(id, config)));
}
if (!(boolean) config.getOrDefault("enable", true)) {
continue;
}
String node = cached.prefix() + "." + key;
ResourceConfigUtils.runCatching(
cached.filePath(),
node,
() -> parseSection(cached.pack(), cached.filePath(), node, id, MiscUtils.castToMap(CraftEngine.instance().templateManager().applyTemplates(id, config), false)),
() -> GsonHelper.get().toJson(section)
);
}
}
protected abstract void parseSection(Pack pack, Path path, String node, Key id, Map<String, Object> section) throws LocalizedException;
}

View File

@@ -0,0 +1,25 @@
package net.momirealms.craftengine.core.plugin.config;
import net.momirealms.craftengine.core.pack.CachedConfigSection;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.plugin.locale.LocalizedException;
import net.momirealms.craftengine.core.util.GsonHelper;
import net.momirealms.craftengine.core.util.ResourceConfigUtils;
import java.nio.file.Path;
import java.util.Map;
public abstract class SectionConfigParser extends AbstractConfigParser {
@Override
protected void parseSection(CachedConfigSection cached) {
ResourceConfigUtils.runCatching(
cached.filePath(),
cached.prefix(),
() -> parseSection(cached.pack(), cached.filePath(), cached.config()),
() -> GsonHelper.get().toJson(cached.config())
);
}
protected abstract void parseSection(Pack pack, Path path, Map<String, Object> section) throws LocalizedException;
}

View File

@@ -3,6 +3,7 @@ package net.momirealms.craftengine.core.plugin.config.template;
import net.momirealms.craftengine.core.pack.LoadingSequence;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.plugin.config.ConfigParser;
import net.momirealms.craftengine.core.plugin.config.IdObjectConfigParser;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MiscUtils;
@@ -36,7 +37,7 @@ public class TemplateManagerImpl implements TemplateManager {
return this.templateParser;
}
public class TemplateParser implements ConfigParser {
public class TemplateParser extends IdObjectConfigParser {
public static final String[] CONFIG_SECTION_NAME = new String[] {"templates", "template"};
@Override
@@ -50,14 +51,9 @@ public class TemplateManagerImpl implements TemplateManager {
}
@Override
public boolean supportsParsingObject() {
return true;
}
@Override
public void parseObject(Pack pack, Path path, Key id, Object obj) {
public void parseObject(Pack pack, Path path, String node, Key id, Object obj) {
if (templates.containsKey(id)) {
throw new LocalizedResourceConfigException("warning.config.template.duplicate", path.toString(), id.toString());
throw new LocalizedResourceConfigException("warning.config.template.duplicate");
}
// 预处理会将 string类型的键或值解析为ArgumentString以加速模板应用。所以处理后不可能存在String类型。
templates.put(id, preprocessUnknownValue(obj));

View File

@@ -4,7 +4,9 @@ import net.momirealms.craftengine.core.pack.LoadingSequence;
import net.momirealms.craftengine.core.pack.Pack;
import net.momirealms.craftengine.core.plugin.Manageable;
import net.momirealms.craftengine.core.plugin.config.ConfigParser;
import net.momirealms.craftengine.core.plugin.config.IdObjectConfigParser;
import net.momirealms.craftengine.core.plugin.locale.LocalizedException;
import net.momirealms.craftengine.core.util.Key;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
@@ -34,7 +36,7 @@ public class GlobalVariableManager implements Manageable {
return this.parser;
}
public class GlobalVariableParser implements ConfigParser {
public class GlobalVariableParser extends IdObjectConfigParser {
public static final String[] CONFIG_SECTION_NAME = new String[] {"global-variables", "global-variable"};
@Override
@@ -48,12 +50,7 @@ public class GlobalVariableManager implements Manageable {
}
@Override
public boolean supportsParsingObject() {
return true;
}
@Override
public void parseObject(Pack pack, Path path, net.momirealms.craftengine.core.util.Key id, Object object) throws LocalizedException {
public void parseObject(Pack pack, Path path, String node, Key id, Object object) throws LocalizedException {
if (object != null) {
GlobalVariableManager.this.globalVariables.put(id.value(), object.toString());
}

View File

@@ -4,7 +4,6 @@ import net.momirealms.craftengine.core.plugin.context.Condition;
import net.momirealms.craftengine.core.plugin.context.Context;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MCUtils;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.util.ResourceConfigUtils;
@@ -18,7 +17,7 @@ public class AllOfCondition<CTX extends Context> implements Condition<CTX> {
protected final Predicate<CTX> condition;
public AllOfCondition(List<? extends Condition<CTX>> conditions) {
this.condition = MCUtils.allOf(conditions);
this.condition = MiscUtils.allOf(conditions);
}
@Override

View File

@@ -4,7 +4,6 @@ import net.momirealms.craftengine.core.plugin.context.Condition;
import net.momirealms.craftengine.core.plugin.context.Context;
import net.momirealms.craftengine.core.plugin.locale.LocalizedResourceConfigException;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MCUtils;
import net.momirealms.craftengine.core.util.MiscUtils;
import net.momirealms.craftengine.core.util.ResourceConfigUtils;
@@ -18,7 +17,7 @@ public class AnyOfCondition<CTX extends Context> implements Condition<CTX> {
protected final Predicate<CTX> condition;
public AnyOfCondition(List<? extends Condition<CTX>> conditions) {
this.condition = MCUtils.anyOf(conditions);
this.condition = MiscUtils.anyOf(conditions);
}
@Override

View File

@@ -2,8 +2,8 @@ package net.momirealms.craftengine.core.plugin.context.condition;
import net.momirealms.craftengine.core.block.CustomBlock;
import net.momirealms.craftengine.core.block.ImmutableBlockState;
import net.momirealms.craftengine.core.block.StatePropertyAccessor;
import net.momirealms.craftengine.core.block.properties.Property;
import net.momirealms.craftengine.core.block.state.StatePropertyAccessor;
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;

View File

@@ -45,6 +45,7 @@ public class EventFunctions {
register(CommonFunctions.TELEPORT, new TeleportFunction.FactoryImpl<>(EventConditions::fromMap));
register(CommonFunctions.SET_VARIABLE, new SetVariableFunction.FactoryImpl<>(EventConditions::fromMap));
register(CommonFunctions.TOAST, new ToastFunction.FactoryImpl<>(EventConditions::fromMap));
register(CommonFunctions.DAMAGE, new DamageFunction.FactoryImpl<>(EventConditions::fromMap));
}
public static void register(Key key, FunctionFactory<PlayerOptionalContext> factory) {

View File

@@ -2,7 +2,6 @@ package net.momirealms.craftengine.core.plugin.context.function;
import net.momirealms.craftengine.core.plugin.context.Condition;
import net.momirealms.craftengine.core.plugin.context.Context;
import net.momirealms.craftengine.core.util.MCUtils;
import net.momirealms.craftengine.core.util.MiscUtils;
import java.util.ArrayList;
@@ -16,7 +15,7 @@ public abstract class AbstractConditionalFunction<CTX extends Context> implement
public AbstractConditionalFunction(List<Condition<CTX>> predicates) {
this.predicates = predicates;
this.compositePredicates = MCUtils.allOf(predicates);
this.compositePredicates = MiscUtils.allOf(predicates);
}
@Override

View File

@@ -5,8 +5,6 @@ import net.momirealms.craftengine.core.plugin.context.*;
import net.momirealms.craftengine.core.plugin.context.parameter.DirectContextParameters;
import net.momirealms.craftengine.core.plugin.context.selector.PlayerSelector;
import net.momirealms.craftengine.core.plugin.context.selector.PlayerSelectors;
import net.momirealms.craftengine.core.plugin.context.text.TextProvider;
import net.momirealms.craftengine.core.plugin.context.text.TextProviders;
import net.momirealms.craftengine.core.util.AdventureHelper;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.ResourceConfigUtils;
@@ -16,12 +14,12 @@ import java.util.List;
import java.util.Map;
public class ActionBarFunction<CTX extends Context> extends AbstractConditionalFunction<CTX> {
private final TextProvider message;
private final String message;
private final PlayerSelector<CTX> selector;
public ActionBarFunction(List<Condition<CTX>> predicates, @Nullable PlayerSelector<CTX> selector, TextProvider messages) {
public ActionBarFunction(List<Condition<CTX>> predicates, @Nullable PlayerSelector<CTX> selector, String message) {
super(predicates);
this.message = messages;
this.message = message;
this.selector = selector;
}
@@ -29,12 +27,12 @@ public class ActionBarFunction<CTX extends Context> extends AbstractConditionalF
public void runInternal(CTX ctx) {
if (this.selector == null) {
ctx.getOptionalParameter(DirectContextParameters.PLAYER).ifPresent(it -> {
it.sendActionBar(AdventureHelper.miniMessage().deserialize(this.message.get(ctx), ctx.tagResolvers()));
it.sendActionBar(AdventureHelper.miniMessage().deserialize(this.message, ctx.tagResolvers()));
});
} else {
for (Player viewer : this.selector.get(ctx)) {
RelationalContext relationalContext = ViewerContext.of(ctx, PlayerOptionalContext.of(viewer));
viewer.sendActionBar(AdventureHelper.miniMessage().deserialize(this.message.get(relationalContext), relationalContext.tagResolvers()));
viewer.sendActionBar(AdventureHelper.miniMessage().deserialize(this.message, relationalContext.tagResolvers()));
}
}
}
@@ -53,7 +51,7 @@ public class ActionBarFunction<CTX extends Context> extends AbstractConditionalF
@Override
public Function<CTX> create(Map<String, Object> arguments) {
String message = ResourceConfigUtils.requireNonEmptyStringOrThrow(ResourceConfigUtils.get(arguments, "actionbar", "message"), "warning.config.function.actionbar.missing_actionbar");
return new ActionBarFunction<>(getPredicates(arguments), PlayerSelectors.fromObject(arguments.get("target"), conditionFactory()), TextProviders.fromString(message));
return new ActionBarFunction<>(getPredicates(arguments), PlayerSelectors.fromObject(arguments.get("target"), conditionFactory()), message);
}
}
}

View File

@@ -7,7 +7,7 @@ import net.momirealms.craftengine.core.plugin.context.number.NumberProvider;
import net.momirealms.craftengine.core.plugin.context.number.NumberProviders;
import net.momirealms.craftengine.core.plugin.context.parameter.DirectContextParameters;
import net.momirealms.craftengine.core.util.Key;
import net.momirealms.craftengine.core.util.MCUtils;
import net.momirealms.craftengine.core.util.MiscUtils;
import java.util.List;
import java.util.Map;
@@ -28,7 +28,7 @@ public class BreakBlockFunction<CTX extends Context> extends AbstractConditional
@Override
public void runInternal(CTX ctx) {
Optional<Player> optionalPlayer = ctx.getOptionalParameter(DirectContextParameters.PLAYER);
optionalPlayer.ifPresent(player -> player.breakBlock(MCUtils.fastFloor(x.getDouble(ctx)), MCUtils.fastFloor(y.getDouble(ctx)), MCUtils.fastFloor(z.getDouble(ctx))));
optionalPlayer.ifPresent(player -> player.breakBlock(MiscUtils.fastFloor(x.getDouble(ctx)), MiscUtils.fastFloor(y.getDouble(ctx)), MiscUtils.fastFloor(z.getDouble(ctx))));
}
@Override

Some files were not shown because too many files have changed in this diff Show More