Auto generate skull resource pack

Change `davchoo` to `geyser` in geometry
This commit is contained in:
davchoo 2022-07-08 14:49:19 -04:00
parent 49f7f6d2f9
commit 9e0af6e098
No known key found for this signature in database
GPG key ID: A0168C8E45799B7D
20 changed files with 651 additions and 24 deletions

View file

@ -27,6 +27,7 @@ package org.geysermc.geyser.pack;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.util.FileUtils;
import org.geysermc.geyser.text.GeyserLocale;
@ -77,13 +78,18 @@ public class ResourcePack {
}
List<Path> resourcePacks;
try {
resourcePacks = Files.walk(directory).collect(Collectors.toList());
try (Stream<Path> stream = Files.walk(directory)) {
resourcePacks = stream.collect(Collectors.toList());
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().error("Could not list packs directory", e);
return;
}
Path skullResourcePack = SkullResourcePackManager.createResourcePack(BlockRegistries.CUSTOM_SKULLS.get().keySet());
if (skullResourcePack != null) {
resourcePacks.add(skullResourcePack);
}
GeyserLoadResourcePacksEvent event = new GeyserLoadResourcePacksEvent(resourcePacks);
GeyserImpl.getInstance().eventBus().fire(event);
@ -95,11 +101,8 @@ public class ResourcePack {
pack.sha256 = FileUtils.calculateSHA256(file);
Stream<? extends ZipEntry> stream = null;
try {
ZipFile zip = new ZipFile(file);
stream = zip.stream();
try (ZipFile zip = new ZipFile(file);
Stream<? extends ZipEntry> stream = zip.stream()) {
stream.forEach((x) -> {
if (x.getName().contains("manifest.json")) {
try {
@ -121,10 +124,6 @@ public class ResourcePack {
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.resource_pack.broken", file.getName()));
e.printStackTrace();
} finally {
if (stream != null) {
stream.close();
}
}
}
}

View file

@ -71,7 +71,7 @@ public class ResourcePackManifest {
@ToString
public static class Module {
private String description;
private String name;
private String type;
private UUID uuid;
private int[] version;
}

View file

@ -0,0 +1,208 @@
/*
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.pack;
import it.unimi.dsi.fastutil.Pair;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.skin.SkinProvider;
import org.geysermc.geyser.util.FileUtils;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
import java.util.function.UnaryOperator;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
public class SkullResourcePackManager {
private static final long RESOURCE_PACK_VERSION = 4;
@SuppressWarnings("ResultOfMethodCallIgnored")
public static Path createResourcePack(Set<String> skins) {
Path packPath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("player_skulls.mcpack");
File packFile = packPath.toFile();
if (skins.isEmpty()) {
packFile.delete(); // No need to keep resource pack
GeyserImpl.getInstance().getLogger().debug("No skins to create player skull resource pack.");
return null;
}
if (packFile.exists() && canReusePack(skins, packFile)) {
GeyserImpl.getInstance().getLogger().info("Reusing cached player skull resource pack.");
return packPath;
}
// We need to create the resource pack from scratch
GeyserImpl.getInstance().getLogger().info("Creating skull resource pack.");
packFile.delete();
try (ZipOutputStream zipOS = new ZipOutputStream(Files.newOutputStream(packPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE))) {
addBaseResources(zipOS, skins);
addSkinTextures(zipOS, skins);
addAttachables(zipOS, skins);
GeyserImpl.getInstance().getLogger().info("Finished creating skull resource pack.");
return packPath;
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().error("Unable to create player skull resource pack!", e);
packFile.delete();
}
return null;
}
private static void addBaseResources(ZipOutputStream zipOS, Set<String> skins) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(GeyserImpl.getInstance().getBootstrap().getResource("bedrock/skull_resource_pack_files.txt")))) {
List<String> lines = reader.lines().toList();
for (String path : lines) {
ZipEntry entry = new ZipEntry(path);
zipOS.putNextEntry(entry);
String resourcePath = "bedrock/" + path;
switch (path) {
case "skull_resource_pack/manifest.json" ->
fillTemplate(zipOS, resourcePath, template -> fillManifestJson(template, skins));
case "skull_resource_pack/textures/terrain_texture.json" ->
fillTemplate(zipOS, resourcePath, template -> fillTerrainTextureJson(template, skins));
default -> zipOS.write(FileUtils.readAllBytes(resourcePath));
}
zipOS.closeEntry();
}
}
}
private static void addAttachables(ZipOutputStream zipOS, Set<String> skins) throws IOException {
String template = new String(FileUtils.readAllBytes("bedrock/skull_resource_pack/attachables/template_attachable.json"), StandardCharsets.UTF_8);
for (String skinHash : skins) {
ZipEntry entry = new ZipEntry("skull_resource_pack/attachables/" + skinHash + ".json");
zipOS.putNextEntry(entry);
zipOS.write(fillAttachableJson(template, skinHash).getBytes(StandardCharsets.UTF_8));
zipOS.closeEntry();
}
}
private static void addSkinTextures(ZipOutputStream zipOS, Set<String> skins) throws IOException {
Path skullSkinCache = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("player_skulls");
Files.createDirectories(skullSkinCache);
for (String skin : skins) {
Path skinPath = skullSkinCache.resolve(skin + ".png");
if (!Files.exists(skinPath)) {
// TODO this should go somewhere else and be async somehow
BufferedImage image = SkinProvider.requestImage("http://textures.minecraft.net/texture/" + skin, null);
ImageIO.write(image, "png", skinPath.toFile());
GeyserImpl.getInstance().getLogger().debug("Cached player skull to file " + skinPath + " for " + skin);
}
ZipEntry entry = new ZipEntry("skull_resource_pack/textures/blocks/" + skin + ".png");
zipOS.putNextEntry(entry);
zipOS.write(FileUtils.readAllBytes(skinPath.toFile()));
zipOS.closeEntry();
}
}
private static void fillTemplate(ZipOutputStream zipOS, String path, UnaryOperator<String> filler) throws IOException {
String template = new String(FileUtils.readAllBytes(path), StandardCharsets.UTF_8);
String result = filler.apply(template);
zipOS.write(result.getBytes(StandardCharsets.UTF_8));
}
private static String fillAttachableJson(String template, String skinHash) {
return template.replace("${identifier}", "geyser:player_skull_" + skinHash) // TOOD use CustomSkull for this
.replace("${texture}", skinHash);
}
private static String fillManifestJson(String template, Set<String> skins) {
Pair<UUID, UUID> uuids = generatePackUUIDs(skins);
return template.replace("${uuid1}", uuids.first().toString())
.replace("${uuid2}", uuids.second().toString());
}
private static String fillTerrainTextureJson(String template, Set<String> skins) {
StringBuilder textures = new StringBuilder();
for (String skinHash : skins) {
String texture = String.format("\"geyser.%s_player_skin\":{\"textures\":\"textures/blocks/%s\"},", skinHash, skinHash);
textures.append(texture);
}
if (textures.length() != 0) {
// Remove trailing comma
textures.deleteCharAt(textures.length() - 1);
}
return template.replace("${texture_data}", textures);
}
private static Pair<UUID, UUID> generatePackUUIDs(Set<String> skins) {
UUID uuid1 = UUID.randomUUID();
UUID uuid2 = UUID.randomUUID();
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
for (int i = 0; i < 8; i++) {
md.update((byte) ((RESOURCE_PACK_VERSION >> (i * 8)) & 0xFF));
}
skins.stream()
.sorted()
.map(hash -> hash.getBytes(StandardCharsets.UTF_8))
.forEach(md::update);
ByteBuffer skinHashes = ByteBuffer.wrap(md.digest());
uuid1 = new UUID(skinHashes.getLong(), skinHashes.getLong());
uuid2 = new UUID(skinHashes.getLong(), skinHashes.getLong());
} catch (NoSuchAlgorithmException e) {
GeyserImpl.getInstance().getLogger().severe("Unable to get SHA-256 Message Digest instance! Bedrock players will have to re-downloaded the player skull resource pack after each server restart.", e);
}
return Pair.of(uuid1, uuid2);
}
private static boolean canReusePack(Set<String> skins, File packFile) {
Pair<UUID, UUID> uuids = generatePackUUIDs(skins);
try (ZipFile zipFile = new ZipFile(packFile)) {
Optional<? extends ZipEntry> manifestEntry = zipFile.stream()
.filter(entry -> entry.getName().contains("manifest.json"))
.findFirst();
if (manifestEntry.isPresent()) {
ResourcePackManifest manifest = FileUtils.loadJson(zipFile.getInputStream(manifestEntry.get()), ResourcePackManifest.class);
if (!uuids.first().equals(manifest.getHeader().getUuid())) {
return false;
}
Optional<UUID> resourceUUID = manifest.getModules().stream()
.filter(module -> "resources".equals(module.getType()))
.findFirst()
.map(ResourcePackManifest.Module::getUuid);
return resourceUUID.isPresent() && uuids.second().equals(resourceUUID.get());
}
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().debug("Cached player skull resource pack was invalid! The pack will be recreated.");
}
return false;
}
}

View file

@ -22,6 +22,7 @@
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.registry.type;
import lombok.Data;
@ -47,8 +48,8 @@ public class CustomSkull {
private CustomBlockData customBlockData;
private static final String DIRECTION_PROPERTY = "davchoo:direction";
private static final String TYPE_PROPERTY = "davchoo:type";
private static final String DIRECTION_PROPERTY = "geyser_skull:direction";
private static final String TYPE_PROPERTY = "geyser_skull:type";
private static final int[] ROTATIONS = {0, -90, 180, 90};
@ -57,7 +58,7 @@ public class CustomSkull {
CustomBlockComponents components = new GeyserCustomBlockComponents.CustomBlockComponentsBuilder()
.destroyTime(1.5f)
.materialInstances(Map.of("*", new MaterialInstance("davchoo." + skinHash + "_player_skin", "alpha_test", true, true)))
.materialInstances(Map.of("*", new MaterialInstance("geyser." + skinHash + "_player_skin", "alpha_test", true, true)))
.lightFilter(0)
.build();
@ -67,7 +68,7 @@ public class CustomSkull {
addWallPermutations(permutations);
customBlockData = new GeyserCustomBlockData.CustomBlockDataBuilder()
.name("player_head_" + skinHash)
.name("player_skull_" + skinHash)
.components(components)
.intProperty(DIRECTION_PROPERTY, IntStream.rangeClosed(0, 15).boxed().toList())
.intProperty(TYPE_PROPERTY, IntStream.rangeClosed(0, 2).boxed().toList())
@ -106,7 +107,7 @@ public class CustomSkull {
private void addDefaultPermutation(List<CustomBlockPermutation> permutations) {
CustomBlockComponents components = new GeyserCustomBlockComponents.CustomBlockComponentsBuilder()
.geometry("geometry.davchoo.player_head_hand")
.geometry("geometry.geyser.player_skull_hand")
.rotation(new RotationComponent(0, 180, 0))
.build();
@ -132,7 +133,7 @@ public class CustomSkull {
CustomBlockComponents components = new GeyserCustomBlockComponents.CustomBlockComponentsBuilder()
.aimCollision(box)
.entityCollision(box)
.geometry("geometry.davchoo.player_head_floor_" + quadrantNames[i])
.geometry("geometry.geyser.player_skull_floor_" + quadrantNames[i])
.rotation(rotation)
.build();
@ -158,7 +159,7 @@ public class CustomSkull {
CustomBlockComponents components = new GeyserCustomBlockComponents.CustomBlockComponentsBuilder()
.aimCollision(box)
.entityCollision(box)
.geometry("geometry.davchoo.player_head_wall")
.geometry("geometry.geyser.player_skull_wall")
.rotation(rotation)
.build();

View file

@ -82,6 +82,7 @@ public class SkullCache {
Skull skull = skulls.computeIfAbsent(position, Skull::new);
if (!texturesProperty.equals(skull.texturesProperty)) {
skull.texturesProperty = texturesProperty;
skull.skinHash = null;
try {
SkinManager.GameProfileData gameProfileData = SkinManager.GameProfileData.loadFromJson(texturesProperty);
if (gameProfileData != null && gameProfileData.skinUrl() != null) {
@ -89,14 +90,12 @@ public class SkullCache {
skull.skinHash = skinUrl.substring(skinUrl.lastIndexOf('/') + 1);
} else {
session.getGeyser().getLogger().debug("Player skull with invalid Skin tag: " + position + " Textures: " + texturesProperty);
skull.skinHash = null;
}
} catch (IOException e) {
session.getGeyser().getLogger().debug("Player skull with invalid Skin tag: " + position + " Textures: " + texturesProperty);
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
e.printStackTrace();
}
skull.skinHash = null;
}
}
skull.blockState = blockState;

View file

@ -406,7 +406,7 @@ public class SkinProvider {
private static Skin supplySkin(UUID uuid, String textureUrl) {
try {
byte[] skin = requestImage(textureUrl, null);
byte[] skin = requestImageData(textureUrl, null);
return new Skin(uuid, textureUrl, skin, System.currentTimeMillis(), false, false);
} catch (Exception ignored) {} // just ignore I guess
@ -416,7 +416,7 @@ public class SkinProvider {
private static Cape supplyCape(String capeUrl, CapeProvider provider) {
byte[] cape = EMPTY_CAPE.getCapeData();
try {
cape = requestImage(capeUrl, provider);
cape = requestImageData(capeUrl, provider);
} catch (Exception ignored) {
} // just ignore I guess
@ -473,7 +473,7 @@ public class SkinProvider {
}
@SuppressWarnings("ResultOfMethodCallIgnored")
private static byte[] requestImage(String imageUrl, CapeProvider provider) throws Exception {
public static BufferedImage requestImage(String imageUrl, CapeProvider provider) throws IOException {
BufferedImage image = null;
// First see if we have a cached file. We also update the modification stamp so we know when the file was last used
@ -533,6 +533,11 @@ public class SkinProvider {
// TODO remove alpha channel
}
return image;
}
private static byte[] requestImageData(String imageUrl, CapeProvider provider) throws Exception {
BufferedImage image = requestImage(imageUrl, provider);
byte[] data = bufferedImageToImageData(image);
image.flush();
return data;

View file

@ -0,0 +1,14 @@
{
"format_version": "1.8.0",
"animations": {
"animation.geyser.disable": {
"loop": true,
"override_previous_animation": true,
"bones": {
"root": {
"scale": 0
}
}
}
}
}

View file

@ -0,0 +1,80 @@
{
"format_version": "1.8.0",
"animations": {
"animation.geyser.player_skull.head": {
"loop": true,
"bones": {
"root_x": {
"position": [0, 15.95, 0],
"scale": [1.1875, 1.1875, 1.1875]
}
}
},
"animation.geyser.player_skull.thirdperson_main_hand": {
"loop": true,
"bones": {
"root": {
"scale": [0.5, 0.5, 0.5],
"position": [0, 12, -4]
},
"root_x": {
"rotation": [45, 0, 0]
},
"root_y": {
"rotation": [0, 135, 0]
}
}
},
"animation.geyser.player_skull.thirdperson_off_hand": {
"loop": true,
"bones": {
"root": {
"scale": [0.5, 0.5, 0.5],
"position": [0, 12, -4]
},
"root_x": {
"rotation": [45, 0, 0]
},
"root_y": {
"rotation": [0, -135, 0]
}
}
},
"animation.geyser.player_skull.firstperson_main_hand": {
"loop": true,
"bones": {
"root": {
"scale": [0.5, 0.5, 0.5],
"position": [2, 16, 4]
},
"root_x": {
"rotation": [180, 0, 0]
},
"root_y": {
"rotation": [0, 135, 0]
},
"root_z": {
"rotation": [45, 0, 0]
}
}
},
"animation.geyser.player_skull.firstperson_off_hand": {
"loop": true,
"bones": {
"root": {
"scale": [0.5, 0.5, 0.5],
"position": [2, 16, 4]
},
"root_x": {
"rotation": [180, 0, 0]
},
"root_y": {
"rotation": [0, 135, 0]
},
"root_z": {
"rotation": [45, 0, 0]
}
}
}
}
}

View file

@ -0,0 +1,57 @@
{
"format_version": "1.10.0",
"minecraft:attachable": {
"description": {
"identifier": "${identifier}",
"materials": {
"default": "entity_alphatest",
"enchanted": "entity_alphatest"
},
"textures": {
"default": "textures/blocks/${texture}",
"enchanted": "textures/misc/enchanted_item_glint"
},
"geometry": {
"default": "geometry.geyser.player_skull"
},
"scripts": {
"pre_animation": [
"v.main_hand = c.item_slot == 'main_hand';",
"v.off_hand = c.item_slot == 'off_hand';",
"v.head = c.item_slot == 'head';"
],
"animate": [
{
"thirdperson_main_hand": "v.main_hand && !c.is_first_person"
},
{
"thirdperson_off_hand": "v.off_hand && !c.is_first_person"
},
{
"thirdperson_head": "v.head && !c.is_first_person"
},
{
"firstperson_main_hand": "v.main_hand && c.is_first_person"
},
{
"firstperson_off_hand": "v.off_hand && c.is_first_person"
},
{
"firstperson_head": "v.head && c.is_first_person || query.is_owner_identifier_any('minecraft:player')"
}
]
},
"animations": {
"thirdperson_main_hand": "animation.geyser.player_skull.thirdperson_main_hand",
"thirdperson_off_hand": "animation.geyser.player_skull.thirdperson_off_hand",
"thirdperson_head": "animation.geyser.player_skull.head",
"firstperson_main_hand": "animation.geyser.player_skull.firstperson_main_hand",
"firstperson_off_hand": "animation.geyser.player_skull.firstperson_off_hand",
"firstperson_head": "animation.geyser.disable"
},
"render_controllers": [
"controller.render.item_default"
]
}
}
}

View file

@ -0,0 +1,17 @@
{
"format_version": 2,
"header": {
"name": "Geyser Player Skull Resource Pack",
"description": "Auto-generated resource pack to support player skulls as custom blocks",
"uuid": "${uuid1}",
"version": [1, 0, 0],
"min_engine_version": [1, 16, 0]
},
"modules": [
{
"type": "resources",
"uuid": "${uuid2}",
"version": [1, 0, 0]
}
]
}

View file

@ -0,0 +1,50 @@
{
"format_version": "1.16.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.geyser.player_skull",
"texture_width": 64,
"texture_height": 64
},
"bones": [
{
"name": "root",
"binding": "c.item_slot == 'head' ? 'head' : q.item_slot_to_bone_name(c.item_slot)",
"pivot": [0, 8, 0]
},
{
"name": "root_x",
"parent": "root",
"pivot": [0, 8, 0]
},
{
"name": "root_y",
"parent": "root_x",
"pivot": [0, 8, 0]
},
{
"name": "root_z",
"parent": "root_y",
"pivot": [0, 8, 0]
},
{
"name": "player_skull",
"parent": "root_z",
"pivot": [0, 24, 0],
"cubes": [
{"origin": [-4, 8, -4], "size": [8, 8, 8], "uv": [0, 0]}
]
},
{
"name": "player_skull_hat",
"parent": "player_skull",
"pivot": [0, 24, 0],
"cubes": [
{"origin": [-4, 8, -4], "size": [8, 8, 8], "uv": [32, 0], "inflate": 0.25}
]
}
]
}
]
}

View file

@ -0,0 +1,29 @@
{
"format_version": "1.12.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.geyser.player_skull_floor_a",
"texture_width": 64,
"texture_height": 64
},
"bones": [
{
"name": "head",
"pivot": [0, 24, 0],
"cubes": [
{"origin": [-4, 0.5, -4], "size": [8, 8, 8], "uv": [0, 0]}
]
},
{
"name": "hat",
"parent": "head",
"pivot": [0, 24, 0],
"cubes": [
{"origin": [-4, 0.5, -4], "size": [8, 8, 8], "uv": [32, 0], "inflate": 0.5}
]
}
]
}
]
}

View file

@ -0,0 +1,30 @@
{
"format_version": "1.12.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.geyser.player_skull_floor_b",
"texture_width": 64,
"texture_height": 64
},
"bones": [
{
"name": "head",
"pivot": [0, 24, 0],
"rotation": [0, 22.5, 0],
"cubes": [
{"origin": [-4, 0.5, -4], "size": [8, 8, 8], "uv": [0, 0]}
]
},
{
"name": "hat",
"parent": "head",
"pivot": [0, 24, 0],
"cubes": [
{"origin": [-4, 0.5, -4], "size": [8, 8, 8], "uv": [32, 0], "inflate": 0.5}
]
}
]
}
]
}

View file

@ -0,0 +1,30 @@
{
"format_version": "1.12.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.geyser.player_skull_floor_c",
"texture_width": 64,
"texture_height": 64
},
"bones": [
{
"name": "head",
"pivot": [0, 24, 0],
"rotation": [0, 45, 0],
"cubes": [
{"origin": [-4, 0.5, -4], "size": [8, 8, 8], "uv": [0, 0]}
]
},
{
"name": "hat",
"parent": "head",
"pivot": [0, 24, 0],
"cubes": [
{"origin": [-4, 0.5, -4], "size": [8, 8, 8], "uv": [32, 0], "inflate": 0.5}
]
}
]
}
]
}

View file

@ -0,0 +1,30 @@
{
"format_version": "1.12.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.geyser.player_skull_floor_d",
"texture_width": 64,
"texture_height": 64
},
"bones": [
{
"name": "head",
"pivot": [0, 24, 0],
"rotation": [0, 67.5, 0],
"cubes": [
{"origin": [-4, 0.5, -4], "size": [8, 8, 8], "uv": [0, 0]}
]
},
{
"name": "hat",
"parent": "head",
"pivot": [0, 24, 0],
"cubes": [
{"origin": [-4, 0.5, -4], "size": [8, 8, 8], "uv": [32, 0], "inflate": 0.5}
]
}
]
}
]
}

View file

@ -0,0 +1,29 @@
{
"format_version": "1.12.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.geyser.player_skull_hand",
"texture_width": 64,
"texture_height": 64
},
"bones": [
{
"name": "head",
"pivot": [0, 24, 0],
"cubes": [
{"origin": [-4, 4, -4], "size": [8, 8, 8], "uv": [0, 0]}
]
},
{
"name": "hat",
"parent": "head",
"pivot": [0, 24, 0],
"cubes": [
{"origin": [-4, 4, -4], "size": [8, 8, 8], "uv": [32, 0], "inflate": 0.5}
]
}
]
}
]
}

View file

@ -0,0 +1,29 @@
{
"format_version": "1.12.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.geyser.player_skull_wall",
"texture_width": 64,
"texture_height": 64
},
"bones": [
{
"name": "head",
"pivot": [0, 24, 0],
"cubes": [
{"origin": [-4, 4, -0.5], "size": [8, 8, 8], "uv": [0, 0]}
]
},
{
"name": "hat",
"parent": "head",
"pivot": [0, 24, 0],
"cubes": [
{"origin": [-4, 4, -0.5], "size": [8, 8, 8], "uv": [32, 0], "inflate": 0.5}
]
}
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -0,0 +1,8 @@
{
"num_mip_levels": 4,
"padding": 8,
"resource_pack_name": "Geyser Player Skull Resource Pack",
"texture_data": {
${texture_data}
}
}

View file

@ -0,0 +1,12 @@
skull_resource_pack/animations/disable.animation.json
skull_resource_pack/animations/player_skull.animation.json
skull_resource_pack/models/blocks/player_skull.geo.json
skull_resource_pack/models/blocks/player_skull_floor_a.geo.json
skull_resource_pack/models/blocks/player_skull_floor_c.geo.json
skull_resource_pack/models/blocks/player_skull_floor_b.geo.json
skull_resource_pack/models/blocks/player_skull_floor_d.geo.json
skull_resource_pack/models/blocks/player_skull_hand.geo.json
skull_resource_pack/models/blocks/player_skull_wall.geo.json
skull_resource_pack/textures/terrain_texture.json
skull_resource_pack/pack_icon.png
skull_resource_pack/manifest.json