Closet block + wardrobe UI + bbmodel cosmetic pipeline
- Closet BlockEntity with open/closed state; BER renders bbmodels via CosmeticRenderLayer - bbmodel parser: format_version 5.0 gate, - ClosetBlock right-click: teleport player to 'player_stand_location' locator, force THIRD_PERSON_FRONT, open wardrobe Screen - ClosetScreen: horizontal tabs, scrollable cosmetic row with drag to scroll, mini player previews via graphics.entity + per State override map in CosmeticRenderLayer, left/right rotate buttons (yaw-only, camera fixed), Toggle button hides previews - Click on player navigation: world to screen project equipped cosmetic bone anchors, jump to matching tab + scroll to that item - Placed test_hat, pike + mock entries to exercise overflow/scroll (clean this up later)main
parent
82414b21c1
commit
42994b15a3
@ -0,0 +1,47 @@
|
||||
{
|
||||
"meta": {
|
||||
"format_version": "5.0",
|
||||
"model_format": "bedrock",
|
||||
"box_uv": true
|
||||
},
|
||||
"name": "player_rig",
|
||||
"model_identifier": "player",
|
||||
"visible_box": [2, 2, 0],
|
||||
"variable_placeholders": "",
|
||||
"variable_placeholder_buttons": [],
|
||||
"bedrock_animation_mode": "entity",
|
||||
"timeline_setups": [],
|
||||
"unhandled_root_fields": {},
|
||||
"resolution": {"width": 64, "height": 64},
|
||||
"elements": [
|
||||
{"name":"head", "box_uv":true, "from":[-4,24,-4], "to":[4,32,4], "origin":[0,24,0], "uv_offset":[0,0], "type":"cube", "uuid":"a1111111-0000-0000-0000-000000000001"},
|
||||
{"name":"hat", "box_uv":true, "from":[-4,24,-4], "to":[4,32,4], "origin":[0,24,0], "uv_offset":[32,0], "inflate":0.5, "type":"cube", "uuid":"a1111111-0000-0000-0000-000000000002"},
|
||||
{"name":"body", "box_uv":true, "from":[-4,12,-2], "to":[4,24,2], "origin":[0,24,0], "uv_offset":[16,16], "type":"cube", "uuid":"a1111111-0000-0000-0000-000000000003"},
|
||||
{"name":"jacket", "box_uv":true, "from":[-4,12,-2], "to":[4,24,2], "origin":[0,24,0], "uv_offset":[16,32], "inflate":0.25,"type":"cube", "uuid":"a1111111-0000-0000-0000-000000000004"},
|
||||
{"name":"right_arm", "box_uv":true, "from":[-8,12,-2], "to":[-4,24,2], "origin":[-5,22,0], "uv_offset":[40,16], "type":"cube", "uuid":"a1111111-0000-0000-0000-000000000005"},
|
||||
{"name":"right_sleeve", "box_uv":true, "from":[-8,12,-2], "to":[-4,24,2], "origin":[-5,22,0], "uv_offset":[40,32], "inflate":0.25,"type":"cube", "uuid":"a1111111-0000-0000-0000-000000000006"},
|
||||
{"name":"left_arm", "box_uv":true, "from":[4,12,-2], "to":[8,24,2], "origin":[5,22,0], "uv_offset":[32,48], "type":"cube", "uuid":"a1111111-0000-0000-0000-000000000007"},
|
||||
{"name":"left_sleeve", "box_uv":true, "from":[4,12,-2], "to":[8,24,2], "origin":[5,22,0], "uv_offset":[48,48], "inflate":0.25,"type":"cube", "uuid":"a1111111-0000-0000-0000-000000000008"},
|
||||
{"name":"right_leg", "box_uv":true, "from":[-3.9,0,-2],"to":[0.1,12,2], "origin":[-1.9,12,0], "uv_offset":[0,16], "type":"cube", "uuid":"a1111111-0000-0000-0000-000000000009"},
|
||||
{"name":"right_pants", "box_uv":true, "from":[-3.9,0,-2],"to":[0.1,12,2], "origin":[-1.9,12,0], "uv_offset":[0,32], "inflate":0.25,"type":"cube", "uuid":"a1111111-0000-0000-0000-00000000000a"},
|
||||
{"name":"left_leg", "box_uv":true, "from":[-0.1,0,-2],"to":[3.9,12,2], "origin":[1.9,12,0], "uv_offset":[16,48], "type":"cube", "uuid":"a1111111-0000-0000-0000-00000000000b"},
|
||||
{"name":"left_pants", "box_uv":true, "from":[-0.1,0,-2],"to":[3.9,12,2], "origin":[1.9,12,0], "uv_offset":[0,48], "inflate":0.25,"type":"cube", "uuid":"a1111111-0000-0000-0000-00000000000c"}
|
||||
],
|
||||
"groups": [
|
||||
{"name":"Head", "uuid":"b2222222-0000-0000-0000-000000000001","origin":[0,24,0], "rotation":[0,0,0],"visibility":true,"color":0,"export":true,"isOpen":true,"children":[]},
|
||||
{"name":"Body", "uuid":"b2222222-0000-0000-0000-000000000002","origin":[0,24,0], "rotation":[0,0,0],"visibility":true,"color":0,"export":true,"isOpen":true,"children":[]},
|
||||
{"name":"RightArm", "uuid":"b2222222-0000-0000-0000-000000000003","origin":[-5,22,0], "rotation":[0,0,0],"visibility":true,"color":0,"export":true,"isOpen":true,"children":[]},
|
||||
{"name":"LeftArm", "uuid":"b2222222-0000-0000-0000-000000000004","origin":[5,22,0], "rotation":[0,0,0],"visibility":true,"color":0,"export":true,"isOpen":true,"children":[]},
|
||||
{"name":"RightLeg", "uuid":"b2222222-0000-0000-0000-000000000005","origin":[-1.9,12,0], "rotation":[0,0,0],"visibility":true,"color":0,"export":true,"isOpen":true,"children":[]},
|
||||
{"name":"LeftLeg", "uuid":"b2222222-0000-0000-0000-000000000006","origin":[1.9,12,0], "rotation":[0,0,0],"visibility":true,"color":0,"export":true,"isOpen":true,"children":[]}
|
||||
],
|
||||
"outliner": [
|
||||
{"uuid":"b2222222-0000-0000-0000-000000000001","name":"Head","isOpen":true,"children":["a1111111-0000-0000-0000-000000000001","a1111111-0000-0000-0000-000000000002"]},
|
||||
{"uuid":"b2222222-0000-0000-0000-000000000002","name":"Body","isOpen":true,"children":["a1111111-0000-0000-0000-000000000003","a1111111-0000-0000-0000-000000000004"]},
|
||||
{"uuid":"b2222222-0000-0000-0000-000000000003","name":"RightArm","isOpen":true,"children":["a1111111-0000-0000-0000-000000000005","a1111111-0000-0000-0000-000000000006"]},
|
||||
{"uuid":"b2222222-0000-0000-0000-000000000004","name":"LeftArm","isOpen":true,"children":["a1111111-0000-0000-0000-000000000007","a1111111-0000-0000-0000-000000000008"]},
|
||||
{"uuid":"b2222222-0000-0000-0000-000000000005","name":"RightLeg","isOpen":true,"children":["a1111111-0000-0000-0000-000000000009","a1111111-0000-0000-0000-00000000000a"]},
|
||||
{"uuid":"b2222222-0000-0000-0000-000000000006","name":"LeftLeg","isOpen":true,"children":["a1111111-0000-0000-0000-00000000000b","a1111111-0000-0000-0000-00000000000c"]}
|
||||
],
|
||||
"textures": []
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package com.razz.dfashion.bbmodel;
|
||||
|
||||
public record BbFace (float[] uv, int texture, int rotation){
|
||||
public record BbFace (float[] uv, Integer texture, int rotation){
|
||||
}
|
||||
|
||||
@ -0,0 +1,171 @@
|
||||
package com.razz.dfashion.bbmodel;
|
||||
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
import net.minecraft.core.Direction;
|
||||
import org.joml.Vector3f;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class BbmodelBaker {
|
||||
|
||||
public static Map<String, ModelPart> bake(Bbmodel model, int texWidth, int texHeight, boolean mirrorX) {
|
||||
Map<String, BbCube> cubeIndex = new HashMap<>();
|
||||
for (BbCube c : model.elements()) cubeIndex.put(c.uuid(), c);
|
||||
|
||||
Map<String, BbGroup> groupIndex = new HashMap<>();
|
||||
for (BbGroup g : model.groups()) groupIndex.put(g.uuid(), g);
|
||||
|
||||
Map<String, ModelPart> result = new HashMap<>();
|
||||
for (BbOutlinerNode node : model.outliner()) {
|
||||
if (node instanceof BbOutlinerNode.GroupRef gref) {
|
||||
BbGroup g = groupIndex.get(gref.uuid());
|
||||
// Top-level: parentOrigin = the group's own origin so the ModelPart's
|
||||
// position resolves to (0, 0, 0). bone.translateAndRotate in the render
|
||||
// layer already places us at the bone, so the ModelPart itself doesn't
|
||||
// need to translate again.
|
||||
result.put(g.name(),
|
||||
buildPart(gref, g, g.origin(), cubeIndex, groupIndex, texWidth, texHeight, mirrorX));
|
||||
}
|
||||
// ElementRef at root = loose cube, skip (no bone to attach to)
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static ModelPart buildPart(
|
||||
BbOutlinerNode.GroupRef gref,
|
||||
BbGroup group,
|
||||
Vector3f parentOrigin,
|
||||
Map<String, BbCube> cubeIndex,
|
||||
Map<String, BbGroup> groupIndex,
|
||||
int texWidth, int texHeight,
|
||||
boolean mirrorX
|
||||
) {
|
||||
List<ModelPart.Cube> cubes = new ArrayList<>();
|
||||
Map<String, ModelPart> children = new LinkedHashMap<>();
|
||||
|
||||
for (BbOutlinerNode child : gref.children()) {
|
||||
switch (child) {
|
||||
case BbOutlinerNode.ElementRef er -> {
|
||||
BbCube bb = cubeIndex.get(er.uuid());
|
||||
if (bb == null) continue; // locator ref, skip
|
||||
|
||||
if (isRotated(bb)) {
|
||||
// ModelPart.Cube is axis-aligned, so per-cube rotation is done
|
||||
// by wrapping the cube in its own one-cube ModelPart whose pose
|
||||
// carries the rotation.
|
||||
ModelPart.Cube inner = bbCubeToCube(bb, bb.origin(), texWidth, texHeight, mirrorX);
|
||||
ModelPart wrapper = new ModelPart(List.of(inner), Map.of());
|
||||
wrapper.setPos(
|
||||
bb.origin().x - group.origin().x,
|
||||
bb.origin().y - group.origin().y,
|
||||
bb.origin().z - group.origin().z
|
||||
);
|
||||
wrapper.setRotation(
|
||||
(float) Math.toRadians(bb.rotation().x),
|
||||
(float) Math.toRadians(bb.rotation().y),
|
||||
(float) Math.toRadians(bb.rotation().z)
|
||||
);
|
||||
children.put("cube_" + bb.uuid(), wrapper);
|
||||
} else {
|
||||
cubes.add(bbCubeToCube(bb, group.origin(), texWidth, texHeight, mirrorX));
|
||||
}
|
||||
}
|
||||
case BbOutlinerNode.GroupRef nested -> {
|
||||
BbGroup ng = groupIndex.get(nested.uuid());
|
||||
children.put(ng.name(),
|
||||
buildPart(nested, ng, group.origin(), cubeIndex, groupIndex, texWidth, texHeight, mirrorX));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ModelPart part = new ModelPart(cubes, children);
|
||||
part.setPos(
|
||||
group.origin().x - parentOrigin.x,
|
||||
group.origin().y - parentOrigin.y,
|
||||
group.origin().z - parentOrigin.z
|
||||
);
|
||||
part.setRotation(
|
||||
(float) Math.toRadians(group.rotation().x),
|
||||
(float) Math.toRadians(group.rotation().y),
|
||||
(float) Math.toRadians(group.rotation().z)
|
||||
);
|
||||
return part;
|
||||
}
|
||||
|
||||
private static boolean isRotated(BbCube bb) {
|
||||
Vector3f r = bb.rotation();
|
||||
return r != null && (r.x != 0f || r.y != 0f || r.z != 0f);
|
||||
}
|
||||
|
||||
private static ModelPart.Cube bbCubeToCube(
|
||||
BbCube bb, Vector3f referenceOrigin, int texWidth, int texHeight, boolean mirrorX
|
||||
) {
|
||||
float x = bb.from().x - referenceOrigin.x;
|
||||
float y = bb.from().y - referenceOrigin.y;
|
||||
float z = bb.from().z - referenceOrigin.z;
|
||||
|
||||
float w = bb.to().x - bb.from().x;
|
||||
float h = bb.to().y - bb.from().y;
|
||||
float d = bb.to().z - bb.from().z;
|
||||
|
||||
// Which faces are visible — texture == -1 means hidden in bbmodel.
|
||||
Set<Direction> visibleFaces = EnumSet.noneOf(Direction.class);
|
||||
if (bb.faces() != null) {
|
||||
bb.faces().forEach((key, face) -> {
|
||||
if (face.texture() != null && face.texture() >= 0) {
|
||||
Direction dir = directionFromName(key);
|
||||
if (dir != null) visibleFaces.add(dir);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Vanilla ctor gives us vertex geometry + placeholder box-UV polygons.
|
||||
// mirrorX=true flips the cube on X so faces orient correctly after the
|
||||
// player render-layer's scale(-1, -1, 1) un-flip. Block-entity renderers
|
||||
// don't apply that pre-flip, so they pass mirrorX=false.
|
||||
ModelPart.Cube cube = new ModelPart.Cube(
|
||||
0, 0, x, y, z, w, h, d, 0, 0, 0, mirrorX, texWidth, texHeight, visibleFaces
|
||||
);
|
||||
|
||||
// Vanilla writes polygons[] in this order, skipping any direction not in visibleFaces.
|
||||
Direction[] order = {
|
||||
Direction.DOWN, Direction.UP, Direction.WEST,
|
||||
Direction.NORTH, Direction.EAST, Direction.SOUTH
|
||||
};
|
||||
|
||||
int idx = 0;
|
||||
for (Direction dir : order) {
|
||||
if (!visibleFaces.contains(dir)) continue;
|
||||
BbFace bbFace = bb.faces().get(nameOf(dir));
|
||||
if (bbFace != null && bbFace.uv() != null && bbFace.uv().length >= 4) {
|
||||
cube.polygons[idx] = new ModelPart.Polygon(
|
||||
cube.polygons[idx].vertices(),
|
||||
bbFace.uv()[0], bbFace.uv()[1],
|
||||
bbFace.uv()[2], bbFace.uv()[3],
|
||||
texWidth, texHeight,
|
||||
mirrorX,
|
||||
dir
|
||||
);
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
|
||||
return cube;
|
||||
}
|
||||
|
||||
private static String nameOf(Direction d) {
|
||||
return d.getName();
|
||||
}
|
||||
|
||||
private static Direction directionFromName(String name) {
|
||||
return switch (name) {
|
||||
case "down" -> Direction.DOWN;
|
||||
case "up" -> Direction.UP;
|
||||
case "north" -> Direction.NORTH;
|
||||
case "south" -> Direction.SOUTH;
|
||||
case "west" -> Direction.WEST;
|
||||
case "east" -> Direction.EAST;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package com.razz.dfashion.block;
|
||||
|
||||
import com.mojang.serialization.MapCodec;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.InteractionResult;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.BaseEntityBlock;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.HorizontalDirectionalBlock;
|
||||
import net.minecraft.world.level.block.Mirror;
|
||||
import net.minecraft.world.level.block.Rotation;
|
||||
import net.minecraft.world.level.block.RenderShape;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.block.state.StateDefinition;
|
||||
import net.minecraft.world.level.block.state.properties.EnumProperty;
|
||||
import net.minecraft.world.phys.BlockHitResult;
|
||||
|
||||
public class ClosetBlock extends BaseEntityBlock {
|
||||
|
||||
public static final MapCodec<ClosetBlock> CODEC = simpleCodec(ClosetBlock::new);
|
||||
public static final EnumProperty<Direction> FACING = HorizontalDirectionalBlock.FACING;
|
||||
|
||||
public ClosetBlock(Properties properties) {
|
||||
super(properties);
|
||||
registerDefaultState(defaultBlockState().setValue(FACING, Direction.NORTH));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MapCodec<? extends BaseEntityBlock> codec() {
|
||||
return CODEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
|
||||
builder.add(FACING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockState getStateForPlacement(BlockPlaceContext ctx) {
|
||||
return defaultBlockState().setValue(FACING, ctx.getHorizontalDirection().getOpposite());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BlockState rotate(BlockState state, Rotation rotation) {
|
||||
return state.setValue(FACING, rotation.rotate(state.getValue(FACING)));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BlockState mirror(BlockState state, Mirror mirror) {
|
||||
return state.rotate(mirror.getRotation(state.getValue(FACING)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
|
||||
return new ClosetBlockEntity(pos, state);
|
||||
}
|
||||
|
||||
// Hides the default block model so only the BlockEntityRenderer draws.
|
||||
@Override
|
||||
protected RenderShape getRenderShape(BlockState state) {
|
||||
return RenderShape.INVISIBLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos,
|
||||
Player player, BlockHitResult hit) {
|
||||
if (level.isClientSide()) {
|
||||
com.razz.dfashion.client.ClosetClientHandler.onRightClick(pos, state);
|
||||
}
|
||||
// Server does nothing — open/closed is a client-side visual tied to the wardrobe
|
||||
// Screen's lifetime (open on Screen init, close on Screen close).
|
||||
return InteractionResult.SUCCESS;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,58 @@
|
||||
package com.razz.dfashion.block;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.network.protocol.Packet;
|
||||
import net.minecraft.network.protocol.game.ClientGamePacketListener;
|
||||
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
|
||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
import net.minecraft.world.level.storage.ValueOutput;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
public class ClosetBlockEntity extends BlockEntity {
|
||||
|
||||
private boolean open = false;
|
||||
|
||||
public ClosetBlockEntity(BlockPos pos, BlockState state) {
|
||||
super(ClosetRegistry.CLOSET_BE.get(), pos, state);
|
||||
}
|
||||
|
||||
public boolean isOpen() {
|
||||
return open;
|
||||
}
|
||||
|
||||
public void setOpen(boolean open) {
|
||||
if (this.open == open) return;
|
||||
this.open = open;
|
||||
setChanged();
|
||||
if (level != null && !level.isClientSide()) {
|
||||
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveAdditional(ValueOutput output) {
|
||||
super.saveAdditional(output);
|
||||
output.putBoolean("open", open);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadAdditional(ValueInput input) {
|
||||
super.loadAdditional(input);
|
||||
this.open = input.getBooleanOr("open", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
|
||||
return saveWithoutMetadata(registries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Packet<ClientGamePacketListener> getUpdatePacket() {
|
||||
return ClientboundBlockEntityDataPacket.create(this);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package com.razz.dfashion.block;
|
||||
|
||||
import com.razz.dfashion.DecoFashion;
|
||||
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.Item;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||
import net.minecraft.world.level.block.state.BlockBehaviour;
|
||||
import net.minecraft.world.level.material.MapColor;
|
||||
import net.neoforged.bus.api.IEventBus;
|
||||
import net.neoforged.neoforge.registries.DeferredBlock;
|
||||
import net.neoforged.neoforge.registries.DeferredHolder;
|
||||
import net.neoforged.neoforge.registries.DeferredItem;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
|
||||
public final class ClosetRegistry {
|
||||
|
||||
public static final DeferredRegister.Blocks BLOCKS =
|
||||
DeferredRegister.createBlocks(DecoFashion.MODID);
|
||||
|
||||
public static final DeferredRegister.Items ITEMS =
|
||||
DeferredRegister.createItems(DecoFashion.MODID);
|
||||
|
||||
public static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITIES =
|
||||
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, DecoFashion.MODID);
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
public static final DeferredBlock<ClosetBlock> CLOSET = BLOCKS.registerBlock(
|
||||
"closet",
|
||||
ClosetBlock::new,
|
||||
BlockBehaviour.Properties.of()
|
||||
.mapColor(MapColor.WOOD)
|
||||
.strength(2.0f)
|
||||
.noOcclusion()
|
||||
);
|
||||
|
||||
public static final DeferredItem<BlockItem> CLOSET_ITEM = ITEMS.registerSimpleBlockItem(CLOSET);
|
||||
|
||||
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<ClosetBlockEntity>> CLOSET_BE =
|
||||
BLOCK_ENTITIES.register(
|
||||
"closet",
|
||||
() -> new BlockEntityType<>(ClosetBlockEntity::new, CLOSET.get())
|
||||
);
|
||||
|
||||
public static void register(IEventBus bus) {
|
||||
BLOCKS.register(bus);
|
||||
ITEMS.register(bus);
|
||||
BLOCK_ENTITIES.register(bus);
|
||||
}
|
||||
|
||||
private ClosetRegistry() {}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
package com.razz.dfashion.client;
|
||||
|
||||
import com.razz.dfashion.DecoFashion;
|
||||
import com.razz.dfashion.block.ClosetBlock;
|
||||
import com.razz.dfashion.block.ClosetBlockEntity;
|
||||
import com.razz.dfashion.client.screen.ClosetScreen;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.player.LocalPlayer;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
|
||||
import org.joml.Vector3f;
|
||||
|
||||
public final class ClosetClientHandler {
|
||||
|
||||
public static void onRightClick(BlockPos pos, BlockState state) {
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
LocalPlayer player = mc.player;
|
||||
if (player == null || mc.level == null) return;
|
||||
|
||||
ClosetModelCache.Baked closed = ClosetModelCache.closed;
|
||||
if (closed != null) {
|
||||
Vector3f anchor = closed.locators().get("player_stand_location");
|
||||
if (anchor != null) {
|
||||
teleportToAnchor(player, pos, state.getValue(ClosetBlock.FACING), anchor);
|
||||
}
|
||||
}
|
||||
|
||||
ClosetBlockEntity be = (mc.level.getBlockEntity(pos) instanceof ClosetBlockEntity cbe) ? cbe : null;
|
||||
mc.setScreen(new ClosetScreen(be));
|
||||
}
|
||||
|
||||
// Must mirror ClosetRenderer's net transform for the point to line up with the
|
||||
// locator's visual position. The renderer does `rotateY(180° - facing.toYRot())`
|
||||
// then `scale(-1, 1, 1)`, which applied to a bbmodel (x, z) point yields:
|
||||
// NORTH → (-x, z)
|
||||
// SOUTH → ( x, -z)
|
||||
// EAST → (-z, -x)
|
||||
// WEST → ( z, x)
|
||||
// These are NOT the same as decocraft's rotatePosition — decocraft doesn't bake
|
||||
// its cubes with mirrorX=true, so it has no compensating scale flip to account for.
|
||||
private static void teleportToAnchor(LocalPlayer player, BlockPos blockPos,
|
||||
Direction facing, Vector3f anchor) {
|
||||
float lx = anchor.x / 16f;
|
||||
float ly = anchor.y / 16f;
|
||||
float lz = anchor.z / 16f;
|
||||
|
||||
double rx, rz;
|
||||
switch (facing) {
|
||||
case NORTH -> { rx = -lx; rz = lz; }
|
||||
case SOUTH -> { rx = lx; rz = -lz; }
|
||||
case EAST -> { rx = -lz; rz = -lx; }
|
||||
case WEST -> { rx = lz; rz = lx; }
|
||||
default -> { rx = -lx; rz = lz; }
|
||||
}
|
||||
|
||||
double worldX = blockPos.getX() + 0.5 + rx;
|
||||
double worldY = blockPos.getY() + ly;
|
||||
double worldZ = blockPos.getZ() + 0.5 + rz;
|
||||
|
||||
DecoFashion.LOGGER.info(
|
||||
"Closet teleport: block={} facing={} anchor(px)={} scaled(X-zeroed)={} rotated=({},{},{}) world=({},{},{})",
|
||||
blockPos, facing, anchor,
|
||||
String.format("(%.3f, %.3f, %.3f)", lx, ly, lz),
|
||||
String.format("%.3f", rx), String.format("%.3f", ly), String.format("%.3f", rz),
|
||||
String.format("%.3f", worldX), String.format("%.3f", worldY), String.format("%.3f", worldZ)
|
||||
);
|
||||
|
||||
player.setPos(worldX, worldY, worldZ);
|
||||
// FACING = direction the doors face. Player stands on that side and faces
|
||||
// the same direction (away from the block interior), so THIRD_PERSON_FRONT
|
||||
// lands the camera out in front of the doors, looking back at the player
|
||||
// with the closet visible behind them.
|
||||
player.setYRot(facing.toYRot());
|
||||
player.setXRot(0f);
|
||||
}
|
||||
|
||||
private ClosetClientHandler() {}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package com.razz.dfashion.client;
|
||||
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
import org.joml.Vector3f;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public final class ClosetModelCache {
|
||||
|
||||
public record Baked(
|
||||
Map<String, ModelPart> parts,
|
||||
Map<String, Vector3f> locators,
|
||||
Identifier texture
|
||||
) {}
|
||||
|
||||
public static volatile Baked closed = null;
|
||||
public static volatile Baked open = null;
|
||||
|
||||
private ClosetModelCache() {}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.razz.dfashion.client;
|
||||
|
||||
import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;
|
||||
import net.minecraft.core.Direction;
|
||||
|
||||
public class ClosetRenderState extends BlockEntityRenderState {
|
||||
public boolean open = false;
|
||||
public Direction facing = Direction.NORTH;
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package com.razz.dfashion.client;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.razz.dfashion.block.ClosetBlock;
|
||||
import com.razz.dfashion.block.ClosetBlockEntity;
|
||||
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
|
||||
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
|
||||
import net.minecraft.client.renderer.blockentity.state.BlockEntityRenderState;
|
||||
import net.minecraft.client.renderer.feature.ModelFeatureRenderer;
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
import net.minecraft.client.renderer.state.level.CameraRenderState;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
public class ClosetRenderer implements BlockEntityRenderer<ClosetBlockEntity, ClosetRenderState> {
|
||||
|
||||
public ClosetRenderer(BlockEntityRendererProvider.Context ctx) {}
|
||||
|
||||
@Override
|
||||
public ClosetRenderState createRenderState() {
|
||||
return new ClosetRenderState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extractRenderState(ClosetBlockEntity be, ClosetRenderState state,
|
||||
float partialTicks, Vec3 cameraPosition,
|
||||
ModelFeatureRenderer.@Nullable CrumblingOverlay breakProgress) {
|
||||
BlockEntityRenderState.extractBase(be, state, breakProgress);
|
||||
state.open = be.isOpen();
|
||||
state.facing = be.getBlockState().getValue(ClosetBlock.FACING);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submit(ClosetRenderState state, PoseStack poseStack,
|
||||
SubmitNodeCollector collector, CameraRenderState camera) {
|
||||
|
||||
ClosetModelCache.Baked baked = state.open ? ClosetModelCache.open : ClosetModelCache.closed;
|
||||
if (baked == null) return;
|
||||
|
||||
poseStack.pushPose();
|
||||
// Center on the block, then rotate to face the placed direction.
|
||||
// Our bbmodels author the closet's door/front on -Z (not Blockbench's
|
||||
// canonical +Z), so rotation = 180° - facing.toYRot() takes bbmodel -Z
|
||||
// to world FACING. ModelPart.Cube already stores vertices pre-scaled by
|
||||
// 1/16, so no additional pixel→block scaling is needed here.
|
||||
poseStack.translate(0.5f, 0.0f, 0.5f);
|
||||
poseStack.mulPose(new org.joml.Quaternionf().rotationY(
|
||||
(float) Math.toRadians(180f - state.facing.toYRot())
|
||||
));
|
||||
// Cubes baked with mirrorX=true have X-flipped vertex winding (matches the
|
||||
// cosmetic pipeline). BEs don't get the entity-renderer pre-flip, so we
|
||||
// apply a compensating X-flip here to keep UVs landing on the right faces.
|
||||
poseStack.scale(-1.0f, 1.0f, 1.0f);
|
||||
|
||||
for (ModelPart part : baked.parts().values()) {
|
||||
collector.submitModelPart(
|
||||
part,
|
||||
poseStack,
|
||||
RenderTypes.entityCutout(baked.texture()),
|
||||
state.lightCoords,
|
||||
OverlayTexture.NO_OVERLAY,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
poseStack.popPose();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.razz.dfashion.client;
|
||||
|
||||
import com.razz.dfashion.cosmetic.CosmeticDefinition;
|
||||
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public final class CosmeticCache {
|
||||
public record Baked(Map<String, ModelPart> parts, Identifier texture) {}
|
||||
|
||||
/** Keyed by cosmetic id (e.g. {@code decofashion:test_hat}). */
|
||||
public static volatile Map<Identifier, Baked> cosmetics = Map.of();
|
||||
|
||||
/** Same keys as {@link #cosmetics} — kept alongside so UI can read display names / categories. */
|
||||
public static volatile Map<Identifier, CosmeticDefinition> catalog = Map.of();
|
||||
|
||||
private CosmeticCache() {}
|
||||
}
|
||||
@ -0,0 +1,129 @@
|
||||
package com.razz.dfashion.client;
|
||||
|
||||
import com.mojang.blaze3d.vertex.PoseStack;
|
||||
import com.razz.dfashion.cosmetic.CosmeticAttachments;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.model.geom.ModelPart;
|
||||
import net.minecraft.client.model.player.PlayerModel;
|
||||
import net.minecraft.client.renderer.SubmitNodeCollector;
|
||||
import net.minecraft.client.renderer.entity.RenderLayerParent;
|
||||
import net.minecraft.client.renderer.entity.layers.RenderLayer;
|
||||
import net.minecraft.client.renderer.entity.state.AvatarRenderState;
|
||||
import net.minecraft.client.renderer.rendertype.RenderTypes;
|
||||
import net.minecraft.client.renderer.texture.OverlayTexture;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class CosmeticRenderLayer extends RenderLayer<AvatarRenderState, PlayerModel> {
|
||||
|
||||
private static final Set<String> WARNED_UNKNOWN_NAMES = new HashSet<>();
|
||||
|
||||
/**
|
||||
* Per-render-state equipped-cosmetic overrides. When a GUI preview (e.g. the wardrobe
|
||||
* Screen) wants to render the player with a specific cosmetic set rather than whatever
|
||||
* is live on the entity, it puts its map here keyed by the render state it's about to
|
||||
* hand to {@code graphics.entity(...)}. This layer checks this map first; if missing,
|
||||
* falls back to the live entity's {@link CosmeticAttachments#EQUIPPED} attachment.
|
||||
* Use {@link IdentityHashMap} semantics so collisions can't happen across states.
|
||||
*/
|
||||
public static final Map<AvatarRenderState, Map<String, net.minecraft.resources.Identifier>> RENDER_OVERRIDES =
|
||||
Collections.synchronizedMap(new IdentityHashMap<>());
|
||||
|
||||
public CosmeticRenderLayer(RenderLayerParent<AvatarRenderState, PlayerModel> parent) {
|
||||
super(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void submit(PoseStack poseStack, SubmitNodeCollector collector, int lightCoords,
|
||||
AvatarRenderState state, float yRot, float xRot) {
|
||||
|
||||
Map<Identifier, CosmeticCache.Baked> cache = CosmeticCache.cosmetics;
|
||||
if (cache.isEmpty()) return;
|
||||
|
||||
Minecraft mc = Minecraft.getInstance();
|
||||
|
||||
// GUI preview override takes precedence: the wardrobe Screen sets this to display
|
||||
// a specific cosmetic in a mini-player box without touching the live attachment.
|
||||
Map<String, Identifier> equipped = RENDER_OVERRIDES.get(state);
|
||||
if (equipped == null) {
|
||||
if (mc.level == null) return;
|
||||
Entity entity = mc.level.getEntity(state.id);
|
||||
if (entity == null) return;
|
||||
equipped = entity.getData(CosmeticAttachments.EQUIPPED.get());
|
||||
}
|
||||
if (equipped.isEmpty()) return;
|
||||
|
||||
PlayerModel model = getParentModel();
|
||||
|
||||
for (Identifier cosmeticId : equipped.values()) {
|
||||
CosmeticCache.Baked cosmetic = cache.get(cosmeticId);
|
||||
if (cosmetic == null) continue; // unknown cosmetic id, skip
|
||||
|
||||
for (Map.Entry<String, ModelPart> entry : cosmetic.parts().entrySet()) {
|
||||
ModelPart bone = findBone(model, entry.getKey());
|
||||
if (bone == null) continue;
|
||||
|
||||
poseStack.pushPose();
|
||||
bone.translateAndRotate(poseStack);
|
||||
// LivingEntityRenderer applied scale(-1, -1, 1) before calling layers.
|
||||
// Un-flip so Blockbench visual-upright coords render correctly.
|
||||
poseStack.scale(-1.0F, -1.0F, 1.0F);
|
||||
collector.submitModelPart(
|
||||
entry.getValue(),
|
||||
poseStack,
|
||||
RenderTypes.entityCutout(cosmetic.texture()),
|
||||
lightCoords,
|
||||
OverlayTexture.NO_OVERLAY,
|
||||
null
|
||||
);
|
||||
poseStack.popPose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ModelPart findBone(PlayerModel model, String rawName) {
|
||||
String name = normalize(rawName);
|
||||
ModelPart bone = switch (name) {
|
||||
case "head" -> model.head;
|
||||
case "body", "waist", "torso" -> model.body;
|
||||
case "right_arm" -> model.rightArm;
|
||||
case "left_arm" -> model.leftArm;
|
||||
case "right_leg" -> model.rightLeg;
|
||||
case "left_leg" -> model.leftLeg;
|
||||
case "hat" -> model.hat;
|
||||
case "jacket" -> model.jacket;
|
||||
case "right_sleeve" -> model.rightSleeve;
|
||||
case "left_sleeve" -> model.leftSleeve;
|
||||
case "right_pants" -> model.rightPants;
|
||||
case "left_pants" -> model.leftPants;
|
||||
// No dedicated cape bone on PlayerModel in 26.1 (the cape is a separate
|
||||
// render layer). Cape cosmetics should attach to the body bone.
|
||||
default -> null;
|
||||
};
|
||||
if (bone == null && WARNED_UNKNOWN_NAMES.add(rawName)) {
|
||||
System.out.println("[DecoFashion] Cosmetic group '" + rawName
|
||||
+ "' (normalized: '" + name + "') doesn't match any player bone; skipping.");
|
||||
}
|
||||
return bone;
|
||||
}
|
||||
|
||||
/** PascalCase / camelCase / UPPER -> snake_case lowercase. */
|
||||
private static String normalize(String name) {
|
||||
StringBuilder sb = new StringBuilder(name.length() + 4);
|
||||
for (int i = 0; i < name.length(); i++) {
|
||||
char c = name.charAt(i);
|
||||
if (i > 0 && Character.isUpperCase(c) && !Character.isUpperCase(name.charAt(i - 1))) {
|
||||
sb.append('_');
|
||||
}
|
||||
sb.append(Character.toLowerCase(c));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package com.razz.dfashion.cosmetic;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.razz.dfashion.DecoFashion;
|
||||
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.neoforged.neoforge.attachment.AttachmentType;
|
||||
import net.neoforged.neoforge.registries.DeferredHolder;
|
||||
import net.neoforged.neoforge.registries.DeferredRegister;
|
||||
import net.neoforged.neoforge.registries.NeoForgeRegistries;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class CosmeticAttachments {
|
||||
|
||||
public static final DeferredRegister<AttachmentType<?>> ATTACHMENT_TYPES =
|
||||
DeferredRegister.create(NeoForgeRegistries.ATTACHMENT_TYPES, DecoFashion.MODID);
|
||||
|
||||
private static final Codec<Map<String, Identifier>> MAP_CODEC =
|
||||
Codec.unboundedMap(Codec.STRING, Identifier.CODEC);
|
||||
|
||||
private static final StreamCodec<RegistryFriendlyByteBuf, Map<String, Identifier>> STREAM_CODEC =
|
||||
ByteBufCodecs.map(HashMap::new, ByteBufCodecs.STRING_UTF8, Identifier.STREAM_CODEC);
|
||||
|
||||
/** Per-player map of category -> equipped cosmetic id. */
|
||||
public static final DeferredHolder<AttachmentType<?>, AttachmentType<Map<String, Identifier>>> EQUIPPED =
|
||||
ATTACHMENT_TYPES.register(
|
||||
"equipped_cosmetics",
|
||||
() -> AttachmentType.<Map<String, Identifier>>builder(() -> new HashMap<>())
|
||||
.serialize(MAP_CODEC.fieldOf("equipped"))
|
||||
.sync(STREAM_CODEC)
|
||||
.copyOnDeath()
|
||||
.build()
|
||||
);
|
||||
|
||||
private CosmeticAttachments() {}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
package com.razz.dfashion.cosmetic;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.razz.dfashion.DecoFashion;
|
||||
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.packs.resources.Resource;
|
||||
import net.minecraft.server.packs.resources.ResourceManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class CosmeticCatalog {
|
||||
|
||||
/** Single catalog file: {@code assets/<ns>/cosmetics.json}. */
|
||||
private static final String CATALOG_PATH = "cosmetics.json";
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
|
||||
.registerTypeAdapter(Identifier.class, new IdentifierDeserializer())
|
||||
.create();
|
||||
|
||||
private CosmeticCatalog() {}
|
||||
|
||||
/**
|
||||
* Loads {@code assets/<ns>/cosmetics.json} — a top-level JSON object where each key is
|
||||
* a cosmetic id (path portion, implicit {@code decofashion:} namespace) and each value
|
||||
* is its {@link CosmeticDefinition}.
|
||||
*/
|
||||
public static Map<Identifier, CosmeticDefinition> loadAll(ResourceManager rm) {
|
||||
Map<Identifier, CosmeticDefinition> result = new HashMap<>();
|
||||
|
||||
Identifier fileId = Identifier.fromNamespaceAndPath(DecoFashion.MODID, CATALOG_PATH);
|
||||
Optional<Resource> res = rm.getResource(fileId);
|
||||
if (res.isEmpty()) {
|
||||
DecoFashion.LOGGER.warn("No cosmetics catalog at {}", fileId);
|
||||
return result;
|
||||
}
|
||||
|
||||
try (Reader reader = res.get().openAsReader()) {
|
||||
Type mapType = new TypeToken<Map<String, CosmeticDefinition>>(){}.getType();
|
||||
Map<String, CosmeticDefinition> defs = GSON.fromJson(reader, mapType);
|
||||
if (defs == null) {
|
||||
DecoFashion.LOGGER.warn("Empty cosmetics catalog: {}", fileId);
|
||||
return result;
|
||||
}
|
||||
for (Map.Entry<String, CosmeticDefinition> entry : defs.entrySet()) {
|
||||
if (entry.getValue() == null) continue;
|
||||
Identifier id = Identifier.fromNamespaceAndPath(DecoFashion.MODID, entry.getKey());
|
||||
result.put(id, entry.getValue());
|
||||
}
|
||||
} catch (IOException | JsonParseException ex) {
|
||||
DecoFashion.LOGGER.error("Failed to parse cosmetics catalog {}", fileId, ex);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static final class IdentifierDeserializer implements JsonDeserializer<Identifier> {
|
||||
@Override
|
||||
public Identifier deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
|
||||
throws JsonParseException {
|
||||
return Identifier.parse(json.getAsString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
package com.razz.dfashion.cosmetic;
|
||||
|
||||
import com.mojang.brigadier.CommandDispatcher;
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.razz.dfashion.DecoFashion;
|
||||
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.event.RegisterCommandsEvent;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@EventBusSubscriber(modid = DecoFashion.MODID)
|
||||
public final class CosmeticCommands {
|
||||
|
||||
@SubscribeEvent
|
||||
static void onRegisterCommands(RegisterCommandsEvent event) {
|
||||
CommandDispatcher<CommandSourceStack> dispatcher = event.getDispatcher();
|
||||
|
||||
dispatcher.register(
|
||||
Commands.literal(DecoFashion.MODID)
|
||||
.then(Commands.literal("equip")
|
||||
.then(Commands.argument("category", StringArgumentType.word())
|
||||
.then(Commands.argument("id", StringArgumentType.greedyString())
|
||||
.executes(CosmeticCommands::equip))))
|
||||
.then(Commands.literal("unequip")
|
||||
.then(Commands.argument("category", StringArgumentType.word())
|
||||
.executes(CosmeticCommands::unequip)))
|
||||
.then(Commands.literal("list")
|
||||
.executes(CosmeticCommands::list))
|
||||
);
|
||||
}
|
||||
|
||||
private static int equip(CommandContext<CommandSourceStack> ctx) {
|
||||
ServerPlayer player = ctx.getSource().getPlayer();
|
||||
if (player == null) {
|
||||
ctx.getSource().sendFailure(Component.literal("Must be run by a player"));
|
||||
return 0;
|
||||
}
|
||||
String category = StringArgumentType.getString(ctx, "category");
|
||||
String idStr = StringArgumentType.getString(ctx, "id");
|
||||
Identifier id = Identifier.parse(idStr);
|
||||
|
||||
Map<String, Identifier> equipped = new HashMap<>(player.getData(CosmeticAttachments.EQUIPPED.get()));
|
||||
equipped.put(category, id);
|
||||
player.setData(CosmeticAttachments.EQUIPPED.get(), equipped);
|
||||
|
||||
ctx.getSource().sendSuccess(
|
||||
() -> Component.literal("Equipped " + id + " in category '" + category + "'"),
|
||||
false
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int unequip(CommandContext<CommandSourceStack> ctx) {
|
||||
ServerPlayer player = ctx.getSource().getPlayer();
|
||||
if (player == null) {
|
||||
ctx.getSource().sendFailure(Component.literal("Must be run by a player"));
|
||||
return 0;
|
||||
}
|
||||
String category = StringArgumentType.getString(ctx, "category");
|
||||
|
||||
Map<String, Identifier> equipped = new HashMap<>(player.getData(CosmeticAttachments.EQUIPPED.get()));
|
||||
Identifier removed = equipped.remove(category);
|
||||
player.setData(CosmeticAttachments.EQUIPPED.get(), equipped);
|
||||
|
||||
ctx.getSource().sendSuccess(
|
||||
() -> Component.literal(removed != null
|
||||
? "Unequipped " + removed + " from '" + category + "'"
|
||||
: "Nothing was equipped in '" + category + "'"),
|
||||
false
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int list(CommandContext<CommandSourceStack> ctx) {
|
||||
ServerPlayer player = ctx.getSource().getPlayer();
|
||||
if (player == null) {
|
||||
ctx.getSource().sendFailure(Component.literal("Must be run by a player"));
|
||||
return 0;
|
||||
}
|
||||
Map<String, Identifier> equipped = player.getData(CosmeticAttachments.EQUIPPED.get());
|
||||
if (equipped.isEmpty()) {
|
||||
ctx.getSource().sendSuccess(() -> Component.literal("No cosmetics equipped"), false);
|
||||
} else {
|
||||
equipped.forEach((cat, id) ->
|
||||
ctx.getSource().sendSuccess(() -> Component.literal(cat + " = " + id), false)
|
||||
);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
private CosmeticCommands() {}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package com.razz.dfashion.cosmetic;
|
||||
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
public record CosmeticDefinition(
|
||||
String displayName,
|
||||
String category,
|
||||
Identifier model,
|
||||
Identifier texture
|
||||
) {}
|
||||
@ -0,0 +1,8 @@
|
||||
{
|
||||
"variants": {
|
||||
"facing=south": { "model": "decofashion:block/closet", "y": 0 },
|
||||
"facing=west": { "model": "decofashion:block/closet", "y": 90 },
|
||||
"facing=north": { "model": "decofashion:block/closet", "y": 180 },
|
||||
"facing=east": { "model": "decofashion:block/closet", "y": 270 }
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,57 @@
|
||||
{
|
||||
"test_hat": {
|
||||
"display_name": "Test Hat",
|
||||
"category": "hat",
|
||||
"model": "decofashion:cosmetic/test_hat.bbmodel",
|
||||
"texture": "decofashion:textures/cosmetic/test_hat.png"
|
||||
},
|
||||
"pike": {
|
||||
"display_name": "Pike",
|
||||
"category": "torso",
|
||||
"model": "decofashion:cosmetic/pike.bbmodel",
|
||||
"texture": "decofashion:textures/cosmetic/pike.png"
|
||||
},
|
||||
|
||||
"mock_hat_01": {"display_name":"Bowler", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_02": {"display_name":"Top Hat", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_03": {"display_name":"Beanie", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_04": {"display_name":"Fedora", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_05": {"display_name":"Cap", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_06": {"display_name":"Crown", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_07": {"display_name":"Tiara", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_08": {"display_name":"Helmet", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_09": {"display_name":"Sombrero", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_10": {"display_name":"Beret", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_11": {"display_name":"Straw", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_12": {"display_name":"Pirate", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_13": {"display_name":"Witch", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_14": {"display_name":"Cowboy", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_15": {"display_name":"Santa", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_16": {"display_name":"Hood", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_17": {"display_name":"Headband", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_18": {"display_name":"Visor", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_19": {"display_name":"Flower", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_hat_20": {"display_name":"Halo", "category":"hat","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
|
||||
"mock_torso_01": {"display_name":"Jacket", "category":"torso","model":"decofashion:cosmetic/pike.bbmodel","texture":"decofashion:textures/cosmetic/pike.png"},
|
||||
"mock_torso_02": {"display_name":"Robe", "category":"torso","model":"decofashion:cosmetic/pike.bbmodel","texture":"decofashion:textures/cosmetic/pike.png"},
|
||||
"mock_torso_03": {"display_name":"Vest", "category":"torso","model":"decofashion:cosmetic/pike.bbmodel","texture":"decofashion:textures/cosmetic/pike.png"},
|
||||
"mock_torso_04": {"display_name":"Coat", "category":"torso","model":"decofashion:cosmetic/pike.bbmodel","texture":"decofashion:textures/cosmetic/pike.png"},
|
||||
"mock_torso_05": {"display_name":"Shirt", "category":"torso","model":"decofashion:cosmetic/pike.bbmodel","texture":"decofashion:textures/cosmetic/pike.png"},
|
||||
"mock_torso_06": {"display_name":"Cape", "category":"torso","model":"decofashion:cosmetic/pike.bbmodel","texture":"decofashion:textures/cosmetic/pike.png"},
|
||||
"mock_torso_07": {"display_name":"Armor", "category":"torso","model":"decofashion:cosmetic/pike.bbmodel","texture":"decofashion:textures/cosmetic/pike.png"},
|
||||
"mock_torso_08": {"display_name":"Dress", "category":"torso","model":"decofashion:cosmetic/pike.bbmodel","texture":"decofashion:textures/cosmetic/pike.png"},
|
||||
"mock_torso_09": {"display_name":"Hoodie", "category":"torso","model":"decofashion:cosmetic/pike.bbmodel","texture":"decofashion:textures/cosmetic/pike.png"},
|
||||
"mock_torso_10": {"display_name":"Tunic", "category":"torso","model":"decofashion:cosmetic/pike.bbmodel","texture":"decofashion:textures/cosmetic/pike.png"},
|
||||
|
||||
"mock_head_01": {"display_name":"Mask", "category":"head","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_head_02": {"display_name":"Glasses", "category":"head","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
"mock_head_03": {"display_name":"Bandana", "category":"head","model":"decofashion:cosmetic/test_hat.bbmodel","texture":"decofashion:textures/cosmetic/test_hat.png"},
|
||||
|
||||
"mock_legs_01": {"display_name":"Pants", "category":"legs","model":"decofashion:cosmetic/pike.bbmodel","texture":"decofashion:textures/cosmetic/pike.png"},
|
||||
"mock_legs_02": {"display_name":"Shorts", "category":"legs","model":"decofashion:cosmetic/pike.bbmodel","texture":"decofashion:textures/cosmetic/pike.png"},
|
||||
"mock_legs_03": {"display_name":"Skirt", "category":"legs","model":"decofashion:cosmetic/pike.bbmodel","texture":"decofashion:textures/cosmetic/pike.png"},
|
||||
|
||||
"mock_feet_01": {"display_name":"Boots", "category":"feet","model":"decofashion:cosmetic/pike.bbmodel","texture":"decofashion:textures/cosmetic/pike.png"},
|
||||
"mock_feet_02": {"display_name":"Sandals", "category":"feet","model":"decofashion:cosmetic/pike.bbmodel","texture":"decofashion:textures/cosmetic/pike.png"}
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
{
|
||||
"textures": {
|
||||
"particle": "decofashion:closet/closet_base_spruce"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
{
|
||||
"parent": "decofashion:block/closet"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 622 B |
Binary file not shown.
|
After Width: | Height: | Size: 320 B |
Loading…
Reference in New Issue