Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,30 @@ public record ExamplePayload(Item item, int count) {
通过 `CubeSelection.enableNamespace("your_mod")` 启用;`registerTargetExclusion` 可按 ID 注册、替换和注销运行时排除规则,而无需丢弃模型缓存。
几何共享、BVH 射线检测和后台轮廓任务均有数量及内存预算。26.1 使用 `CuboidModelElement`、`BlockStateModel` 和高亮状态提取接口。

Cube 资源配置位于 `assets/<namespace>/anvillib/cube_selection.json`。KubeJS 整合包可使用
`kubejs/assets/example/anvillib/cube_selection.json`,通过 `F3+T` 与模型一起重载:

```json
{
"namespaces": { "kubejs": "model" },
"blocks": {
"kubejs:example": "outline",
"kubejs:interaction_proxy": "vanilla"
}
}
```

- `model`:精确模型选取与模型描边;`outline`:原有选取与模型描边;`vanilla`:原有选取与描边。
- 方块规则优先于命名空间规则,未配置项使用 Java API 注册的默认值,默认不启用其他方块。
- 按资源包从低到高逐项合并,同一资源包内按配置资源 ID 的字典序合并,后者覆盖同一键;缺省项保留。
- 配置文件移除后,下次重载恢复下层配置或 Java 默认值。非法字段、模式或资源 ID 会记录配置路径和资源包,整份文件不生效。
- 可以预先配置未安装模组的方块 ID;配置不会注册方块。配置内容仅控制客户端模型选取,不改变服务端碰撞与玩法逻辑。
- `exclude` 的程序排除优先于配置;`registerTargetExclusion` 仍同时排除精确选取与默认描边,不删除模型缓存。
- `registerDynamic` 继续提供动态部件,资源规则不会覆盖 provider;其 `includeStaticModel` 参数仍决定是否合并静态模型。
- `target` 仅返回精确选取目标;`outlineTarget` 同时支持 `model` 与 `outline`,供描边和预览使用;`isEnabled` 表示是否启用模型几何。
- 配置与模型在同一次资源重载完成后发布;不支持的模型或超出既有限额的几何保留原版行为。此配置不增加自定义模型加载器或 BER 的自动解析能力。


### RPC 模块

提供 `@RemoteCallable` 方法注册、双向调用、异步返回值以及 `RPC.invokeSync` / `invokeSyncByName` 同步等待。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public static void highlight(ExtractBlockOutlineRenderStateEvent event) {
if (minecraft.options.hideGui) return;
BlockPos pos = event.getBlockPos();
float partial = CubeSelection.frameFraction(event.getLevel(), minecraft.getDeltaTracker().getGameTimeDeltaPartialTick(true));
CubeSelection.Target target = CubeSelection.target(event.getLevel(), pos, event.getBlockState(), partial);
CubeSelection.Target target = CubeSelection.outlineTarget(event.getLevel(), pos, event.getBlockState(), partial);
if (target == null) return;
List<SelectionPart> parts = target.parts();
PackedOutline[] outlines = new PackedOutline[parts.size()];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import dev.anvilcraft.lib.v2.cube.AnvilLibCube;
import dev.anvilcraft.lib.v2.cube.client.model.ModelSelections;
import net.minecraft.client.Minecraft;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.EventPriority;
import net.neoforged.bus.api.SubscribeEvent;
Expand All @@ -12,17 +13,21 @@

@EventBusSubscriber(modid = AnvilLibCube.MOD_ID, value = Dist.CLIENT)
public final class CubeModelEvents {
private static CubeSelectionRules pendingRules = CubeSelectionRules.EMPTY;
private static ModelSelections.Snapshot pending = new ModelSelections.Snapshot(Map.of(), 0, 0);
private CubeModelEvents() { }

@SubscribeEvent(priority = EventPriority.LOWEST)
public static void prepare(ModelEvent.ModifyBakingResult event) {
pending = ModelSelections.bake(event.getBakingResult().blockStateModels());
CubeSelectionRules loaded = CubeSelectionRules.load(Minecraft.getInstance().getResourceManager());
pending = ModelSelections.bake(event.getBakingResult().blockStateModels(), block -> CubeSelection.isEnabled(block, loaded));
pendingRules = loaded;
}

@SubscribeEvent
public static void apply(ModelEvent.BakingCompleted event) {
CubeSelection.install(pending);
CubeSelection.install(pending, pendingRules);
pendingRules = CubeSelectionRules.EMPTY;
pending = new ModelSelections.Snapshot(Map.of(), 0, 0);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ private CubePicking() { }

/** 只包围准星射线调用,普通碰撞、服务端和投射物射线不读取客户端模型。 */
public static HitResult pick(Entity entity, double range, float partialTick, boolean fluids) {
if (!(entity.level() instanceof ClientLevel level) || !CubeSelection.active() || range > MAX_RANGE) {
if (!(entity.level() instanceof ClientLevel level)) {
return entity.pick(range, partialTick, fluids);
}
Context context = CONTEXT.get();
if (context.level != null) return entity.pick(range, partialTick, fluids);
// 仅描边模式也需要逐帧更新动态几何,不能在清理帧缓存之前跳过精确选取。
CubeSelection.beginFrame(level, partialTick);
if (!CubeSelection.active() || range > MAX_RANGE) {
return entity.pick(range, partialTick, fluids);
}
context.level = level;
context.partialTick = partialTick;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;

/** 仅在客户端初始化时注册命名空间;注册后的模型默认同时启用精确拾取和棱线高亮。 */
/** 资源包控制选取与描边模式;客户端注册接口提供默认启用范围及动态几何。 */
public final class CubeSelection {
public static final int MAX_PARTS = 32;
private static final Set<String> NAMESPACES = ConcurrentHashMap.newKeySet();
Expand All @@ -35,10 +35,12 @@ public final class CubeSelection {
private static final Map<Block, Dynamic> DYNAMIC = new ConcurrentHashMap<>();
private static final OutlineCache OUTLINES = new OutlineCache();
private static final Long2ObjectOpenHashMap<Target> FRAME = new Long2ObjectOpenHashMap<>();
private static CubeSelectionRules rules = CubeSelectionRules.EMPTY;
private static Map<BlockState, ModelSelection> models = Map.of();
private static Map<BlockState, Integer> extended = Map.of();
private static @Nullable ClientLevel frameLevel;
private static int radius;
private static boolean pickingActive;
private static float partialTick;
private static long geometryBytes;
private static int unsupportedStates;
Expand Down Expand Up @@ -91,11 +93,29 @@ public static void registerDynamic(Block block, AABB maximumBounds, boolean incl
throw new IllegalArgumentException("Dynamic bounds including model offset must lie in [-2, 3]");
}
DYNAMIC.put(block, new Dynamic(maximumBounds, includeStaticModel, provider));
radius = Math.max(radius, reach(maximumBounds));
if (mode(block, rules) == CubeSelectionRules.Mode.MODEL) {
pickingActive = true;
radius = Math.max(radius, reach(maximumBounds));
}
}

/** 是否启用模型几何;outline 模式仍需烘焙并可由自定义描边器读取。 */
public static boolean isEnabled(Block block) {
return !EXCLUDED.contains(block) && (NAMESPACES.contains(BuiltInRegistries.BLOCK.getKey(block).getNamespace()) || DYNAMIC.containsKey(block));
return isEnabled(block, rules);
}

static boolean isEnabled(Block block, CubeSelectionRules selectionRules) {
return mode(block, selectionRules) != CubeSelectionRules.Mode.VANILLA;
}

private static CubeSelectionRules.Mode mode(Block block, CubeSelectionRules selectionRules) {
if (EXCLUDED.contains(block)) {
return CubeSelectionRules.Mode.VANILLA;
}
var id = BuiltInRegistries.BLOCK.getKey(block);
CubeSelectionRules.Mode fallback = NAMESPACES.contains(id.getNamespace()) || DYNAMIC.containsKey(block)
? CubeSelectionRules.Mode.MODEL : CubeSelectionRules.Mode.VANILLA;
return selectionRules.mode(id, fallback);
}

public static boolean supportedBounds(AABB bounds) {
Expand All @@ -105,6 +125,11 @@ public static boolean supportedBounds(AABB bounds) {
&& bounds.maxX <= 3 && bounds.maxY <= 3 && bounds.maxZ <= 3;
}

static void install(ModelSelections.Snapshot snapshot, CubeSelectionRules selectionRules) {
rules = selectionRules;
install(snapshot);
}

public static void install(ModelSelections.Snapshot snapshot) {
OUTLINES.clear();
FRAME.clear();
Expand All @@ -114,15 +139,25 @@ public static void install(ModelSelections.Snapshot snapshot) {
unsupportedStates = snapshot.unsupportedStates();
Map<BlockState, Integer> overhang = new IdentityHashMap<>();
radius = 0;
pickingActive = false;
models.forEach((state, selection) -> {
if (mode(state.getBlock(), rules) != CubeSelectionRules.Mode.MODEL) {
return;
}
pickingActive = true;
int reach = reach(selection.bounds());
// 原版模型偏移由位置决定,不可烘入共享几何。
if (state.hasOffsetFunction()) reach = Math.min(2, reach + 1);
if (reach > 0) overhang.put(state, reach);
});
extended = overhang;
for (int value : extended.values()) radius = Math.max(radius, value);
for (Dynamic dynamic : DYNAMIC.values()) radius = Math.max(radius, reach(dynamic.bounds));
DYNAMIC.forEach((block, dynamic) -> {
if (mode(block, rules) == CubeSelectionRules.Mode.MODEL) {
pickingActive = true;
radius = Math.max(radius, reach(dynamic.bounds));
}
});
}

public static void beginFrame(ClientLevel level, float fraction) {
Expand All @@ -139,7 +174,8 @@ public static void clearWorld() {

public static OutlineCache outlines() { return OUTLINES; }
public static int searchRadius() { return radius; }
public static boolean active() { return !models.isEmpty() || !DYNAMIC.isEmpty(); }
/** 当前快照是否需要执行模型精确拾取;仅描边的几何不会启动邻格补扫。 */
public static boolean active() { return pickingActive; }

static @Nullable ModelSelection modelForState(BlockState state) { return models.get(state); }

Expand All @@ -155,12 +191,24 @@ public static void clearWorld() {
static float frameFraction(ClientLevel level, float fallback) { return frameLevel == level ? partialTick : fallback; }

public static boolean extendsBlock(BlockState state) {
if (mode(state.getBlock(), rules) != CubeSelectionRules.Mode.MODEL) {
return false;
}
Dynamic dynamic = DYNAMIC.get(state.getBlock());
return extended.containsKey(state) || dynamic != null && reach(dynamic.bounds) > 0;
}

/** 精确拾取入口;仅描边模式保留方块原有射线选取。 */
public static @Nullable Target target(ClientLevel level, BlockPos pos, BlockState state, float fraction) {
if (isTargetExcluded(state)) return null;
if (mode(state.getBlock(), rules) != CubeSelectionRules.Mode.MODEL) {
return null;
}
return outlineTarget(level, pos, state, fraction);
}

/** 描边与预览共用的几何入口;运行时排除规则仍同时约束选取和默认描边。 */
public static @Nullable Target outlineTarget(ClientLevel level, BlockPos pos, BlockState state, float fraction) {
if (!isEnabled(state.getBlock()) || isTargetExcluded(state)) return null;
if (frameLevel != level || fraction != partialTick) beginFrame(level, fraction);
Target cached = FRAME.get(pos.asLong());
if (cached != null && cached.state == state) return cached;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package dev.anvilcraft.lib.v2.cube.client;

import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.logging.LogUtils;
import net.minecraft.resources.Identifier;
import net.minecraft.server.packs.PackResources;
import net.minecraft.server.packs.resources.Resource;
import net.minecraft.server.packs.resources.ResourceManager;

import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/** 规则与本轮烘焙几何一起发布,避免资源重载过程中使用不同批次的数据。 */
record CubeSelectionRules(Map<String, Mode> namespaces, Map<Identifier, Mode> blocks) {
static final CubeSelectionRules EMPTY = new CubeSelectionRules(Map.of(), Map.of());
private static final String PATH = "anvillib/cube_selection.json";

CubeSelectionRules {
namespaces = Map.copyOf(namespaces);
blocks = Map.copyOf(blocks);
}

Mode mode(Identifier block, Mode fallback) {
return this.blocks.getOrDefault(block, this.namespaces.getOrDefault(block.getNamespace(), fallback));
}

static CubeSelectionRules load(ResourceManager resources) {
List<String> packs = resources.listPacks().map(PackResources::packId).toList();
List<Source> sources = new ArrayList<>();
resources.listResourceStacks("anvillib", id -> id.getPath().equals(PATH)).forEach((id, stack) -> {
for (Resource resource : stack) {
sources.add(new Source(id, resource, packs.indexOf(resource.sourcePackId())));
}
});
// 先按资源包排序,保证不同配置命名空间也遵守资源包优先级;同一包内按资源 ID 排序。
sources.sort(Comparator.comparingInt(Source::priority).thenComparing(source -> source.id().toString()));
CubeSelectionRules rules = EMPTY;
for (Source source : sources) {
try (Reader reader = source.resource().openAsReader()) {
rules = rules.merge(JsonParser.parseReader(reader).getAsJsonObject());
} catch (IOException | RuntimeException exception) {
LogUtils.getLogger().warn("Unable to load cube selection rules {} from {}; keeping lower-priority rules",
source.id(), source.resource().sourcePackId(), exception);
}
}
return rules;
}

CubeSelectionRules merge(JsonObject json) {
for (String key : json.keySet()) {
if (!key.equals("namespaces") && !key.equals("blocks")) {
throw new IllegalArgumentException("Unknown cube selection field: " + key);
}
}
Map<String, Mode> namespaces = new HashMap<>(this.namespaces);
Map<Identifier, Mode> blocks = new HashMap<>(this.blocks);
if (json.has("namespaces")) {
for (var entry : json.getAsJsonObject("namespaces").entrySet()) {
Identifier.fromNamespaceAndPath(entry.getKey(), "cube_selection");
namespaces.put(entry.getKey(), Mode.parse(entry.getValue()));
}
}
if (json.has("blocks")) {
for (var entry : json.getAsJsonObject("blocks").entrySet()) {
blocks.put(Identifier.parse(entry.getKey()), Mode.parse(entry.getValue()));
}
}
// 整份文件校验成功后才替换,非法规则不会只应用一半。
return new CubeSelectionRules(namespaces, blocks);
}

enum Mode {
MODEL, OUTLINE, VANILLA;

static Mode parse(JsonElement value) {
if (!value.isJsonPrimitive() || !value.getAsJsonPrimitive().isString()) {
throw new IllegalArgumentException("Cube selection mode must be a string");
}
return switch (value.getAsString()) {
case "model" -> MODEL;
case "outline" -> OUTLINE;
case "vanilla" -> VANILLA;
default -> throw new IllegalArgumentException("Unknown cube selection mode: " + value);
};
}
}

private record Source(Identifier id, Resource resource, int priority) { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;

public final class ModelSelections {
public static final long MAX_GEOMETRY_BYTES = 16L * 1024 * 1024;
Expand All @@ -36,10 +37,15 @@ public final class ModelSelections {
private ModelSelections(Map<BlockStateModelPart, ModelCapture.Source> sources) { this.sources = sources; }

public static Snapshot bake(Map<BlockState, BlockStateModel> models) {
return bake(models, CubeSelection::isEnabled);
}

/** 使用本轮资源规则筛选模型,不提前修改正在使用的选择快照。 */
public static Snapshot bake(Map<BlockState, BlockStateModel> models, Predicate<Block> enabled) {
ModelSelections builder = new ModelSelections(ModelCapture.take());
Map<BlockState, ModelSelection> states = new IdentityHashMap<>();
for (Block block : BuiltInRegistries.BLOCK) {
if (!CubeSelection.isEnabled(block)) continue;
if (!enabled.test(block)) continue;
for (BlockState state : block.getStateDefinition().getPossibleStates()) {
if (states.size() >= MAX_STATES) { builder.unsupported++; continue; }
BlockStateModel baked = models.get(state);
Expand Down
Loading