Add support for custom blocks and skulls (#3505)

* Super cursed custom skulls custom block

* Rename some stuff

* Attempt to clean up some code

* Remove skull translation events and define custom blocks for custom skulls

Clean up skull block translation a bit

* Auto generate skull resource pack

Change `davchoo` to `geyser` in geometry

* Add config options for custom blocks and custom skull blocks

* Fix formatting and names for player skulls

* Use block states more efficiently for custom skulls

21 block states vs 48 block states

* Clean up custom block api a bit

* Apply some suggestions from Camotoy

* Move custom skull config stuff to its own file

Custom skulls can now be added by username, uuid, and textures

Move skull nbt stuff from requestTexturesFromUsername to
SkullBlockEntityTranslator
Add requestTexturesFromUUID

* Update custom block nbt for v534

* Disable collision box & selection box when box is empty

Fix incorrect collision names used in CustomBlockComponentsBuilder

* Add custom block stuff to provider registry loader

* More API changes

Convert CustomBlockPermutation into a record
Change materialInstances in CustomBlockComponents Builder to
materialInstance
Reuse box components in CustomSkull

* Convert skull floor geometries into a template

Should be easier to modify in needed in the future.

* Crop and reorder skull textures to eliminate unused space

Should reduce memory & storage usage for Bedrock clients

* Revert "Crop and reorder skull textures to eliminate unused space"

This reverts commit 15fd5353e1.

* Use identifier from CustomBlockData in SkullResourcePackManager

* Fix isIncorrectHeldItem check for custom skull blocks

Add defaultBlockState to CustomBlockData

* Fix adding duplicate block states for custom blocks with 0 properties

Remove defaultBlockState CustomBlockState field from
GeyserCustomBlockData since it creates a circular reference

* Add basis for overriding Bedrock block states

Fix missing providers when used in GeyserDefineCustomBlocksEvent

* Fix custom blocks in 1.19.50

* Decouple mappings from items

* Decouple mappings from items

* Null check

* Move to CustomBlockRegistryPopulator

* Remove name_hash from blocksTag/vanillaBlockStates

Fixes creative inventory contents with custom blocks registered

* Limit Bedrock versions to 1.19.40+

Custom blocks were released in 1.19.40

* Un-revert Crop and reorder skull textures to eliminate unused space

Should reduce memory & storage usage for Bedrock clients

Bug with top face flipping + per-face uv's was fixed in 1.19.40+
https://bugs.mojang.com/browse/MCPE-160073
Geometry is still offset by 0.5 to prevent lighting bugs

* Add validation custom block components and s/lightFilter/lightDampening/

Also validate custom block names

* Add display name component and add toggle for client block placing

The display name component allows blocks to use other locale keys.
placeAir will prevent the client from placing the default block state.

* Begin parsing block mappings (still much to do!)

* CustomBlockMapping stores block w/ all states

* Mappings almost :/

* Ok now they work at least

* Read most mapping components

* Block mappings mostly done

* Translate block item

* Add docs for custom blocks

* Add tags

* More docs

* Accidentally added name comp.

* Fix collide box and warn for >16 props

* add registerBlockItemOverride event + refactor

* Inventory overrides for multistate bedrock blocks

* Implement all remaining block components

* Minor cleanup and javadocs

* Update custom skull config example

* Address @Camotoy's review

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Fix light_emission and light_dampening components

* Remove redundant populate method and remove BLOCKS_JSON after last use

* Fix inventories with block state overrides not opening

* API event for skull blocks & let register via URL

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Use skin hash instead of URL

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Address @davchoo's review

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Rework MappingsReader_v1 to avoid passing maps around

* Treat all properties as string properties

There isn't a real need to check for boolean and int properties

* Fix block registry scan in MappingsReader

* Skin hashes can have less than 64 characters?

* Include entry when logging exceptions from block mappings

* Submodule

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Fix block break speeds thanks to @Camotoy

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Temporarily fix build on eclipse so I may work...

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Custom tool breakspeed by server; Closes #3348

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Account for if custom skulls are added on 1st run

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Initial framework for extended collision boxes

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Add some notes for the extended collision box impl

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* We have our extended collision registry

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Notes for me

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Extended collision boxes almost work

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Extended collision boxes actually work

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Consider all hitboxes in calculation

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* X is mirrored...

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Extended collision boxes are much improved

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Upstream fallout

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Address @Redned235's review

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Oops my bad that makes no sense :)

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Ext collision box chunk translation optimization

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Trunc skinhash to 32 chars due to 80 char limit

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Use new transformation cmpnt vs legacy rotation

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* keep arr null on get extcolstor

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Properly handle if extended collision box is below

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Less ugly (realized it can go here)

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Prevent 2x placement due to extended collision box

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Properly build on eclipse via indra

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Ensure enough bits in bedrockData for paletteIDs

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Fix not needed whitespace

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Update license headers to 2023

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Use release indra over snapshot

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Revert "Update license headers to 2023"

This reverts commit f750059e8e.

* Account for collisions in chunk section y0 layer

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Fix extended collision @ air section bottom

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Address @davchoo's review

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Address @rtm516's review

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* More @rtm516's review

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Address @Camotoy's review

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Update javadocs

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Address @davchoo's review

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Lock extended collision to section

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Clear ext col even when air

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Let override vanilla items in creative inventory

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Avoid creating 12 HashSets for every overrided block state

* Super minor nitpicks + Custom Skull NBT fix

* Check custom skull is within Bedrock bounds

Fixes NPE with custom skulls above y=320 or below y=-64

* Add static builder methods to match CustomItemData API

* Upstream

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Initial API setup for modded blocks (no impl yet)

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* More work on nonvanilla blocks (nonfunctional)

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Fix compile

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Update submodules

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Modded reg so far (not done)

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Add non-vanilla registration and fix a few bugs

* Fixes for non-vanilla blocks

* Remove import

* CustomRegPop. go1st for now; must split for modded

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Address silent change to geo component for blocks

Co-Authored-By: Unoqwy <pm@unoqwy.dev>
Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Seperate bedrock, vanilla, & nonvanilla block reg

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Single event

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Impl MaterialInstance as builder per @Redned235

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Added creative category enum & added some missing overrides (#7)

* Add material instance to provider registry

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* oops

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Fix case of correctBedrockIdentifier not found

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Fix docs

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Address @Camotoy's review

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Address review from @davchoo

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Set namespace of custom blocks vs ident direct

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Address review from @rtm516

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* One more

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Remove rogue space

* Geo component as builder

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* use super name

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

* Bump version

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>

---------

Signed-off-by: Joshua Castle <26531652+Kas-tle@users.noreply.github.com>
Signed-off-by: GitHub <noreply@github.com>
Co-authored-by: davchoo <davchoo@users.noreply.github.com>
Co-authored-by: davchoo <4722249+davchoo@users.noreply.github.com>
Co-authored-by: Unoqwy <pm@unoqwy.dev>
Co-authored-by: RednedEpic <redned235@gmail.com>
Co-authored-by: ImDaBigBoss <67973871+ImDaBigBoss@users.noreply.github.com>
Co-authored-by: rtm516 <rtm516@users.noreply.github.com>
This commit is contained in:
Kas-tle 2023-08-21 16:04:08 -07:00 committed by GitHub
parent 661a9b4741
commit 9ddfdf9374
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 6235 additions and 328 deletions

View File

@ -0,0 +1,143 @@
/*
* 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.api.block.custom;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.GeyserApi;
import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents;
import org.geysermc.geyser.api.block.custom.property.CustomBlockProperty;
import org.geysermc.geyser.api.util.CreativeCategory;
import java.util.List;
import java.util.Map;
/**
* This class is used to store data for a custom block.
*/
public interface CustomBlockData {
/**
* Gets the name of the custom block
*
* @return The name of the custom block.
*/
@NonNull String name();
/**
* Gets the identifier of the custom block
*
* @return The identifier of the custom block.
*/
@NonNull String identifier();
/**
* Gets if the custom block is included in the creative inventory
*
* @return If the custom block is included in the creative inventory.
*/
boolean includedInCreativeInventory();
/**
* Gets the item's creative category, or tab id.
*
* @return the item's creative category
*/
@Nullable CreativeCategory creativeCategory();
/**
* Gets the item's creative group.
*
* @return the item's creative group
*/
@Nullable String creativeGroup();
/**
* Gets the components of the custom block
*
* @return The components of the custom block.
*/
@Nullable CustomBlockComponents components();
/**
* Gets the custom block's map of block property names to CustomBlockProperty
* objects
*
* @return The custom block's map of block property names to CustomBlockProperty objects.
*/
@NonNull Map<String, CustomBlockProperty<?>> properties();
/**
* Gets the list of the custom block's permutations
*
* @return The permutations of the custom block.
*/
@NonNull List<CustomBlockPermutation> permutations();
/**
* Gets the custom block's default block state
*
* @return The default block state of the custom block.
*/
@NonNull CustomBlockState defaultBlockState();
/**
* Gets a builder for a custom block state
*
* @return The builder for a custom block state.
*/
CustomBlockState.@NonNull Builder blockStateBuilder();
/**
* Create a Builder for CustomBlockData
*
* @return A CustomBlockData Builder
*/
static CustomBlockData.Builder builder() {
return GeyserApi.api().provider(CustomBlockData.Builder.class);
}
interface Builder {
Builder name(@NonNull String name);
Builder includedInCreativeInventory(boolean includedInCreativeInventory);
Builder creativeCategory(@Nullable CreativeCategory creativeCategory);
Builder creativeGroup(@Nullable String creativeGroup);
Builder components(@NonNull CustomBlockComponents components);
Builder booleanProperty(@NonNull String propertyName);
Builder intProperty(@NonNull String propertyName, List<Integer> values);
Builder stringProperty(@NonNull String propertyName, List<String> values);
Builder permutations(@NonNull List<CustomBlockPermutation> permutations);
CustomBlockData build();
}
}

View File

@ -0,0 +1,39 @@
/*
* 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.api.block.custom;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents;
/**
* This class is used to store a custom block permutations, which contain custom
* block components mapped to a Molang query that should return true or false
*
* @param components The components of the block
* @param condition The Molang query that should return true or false
*/
public record CustomBlockPermutation(@NonNull CustomBlockComponents components, @NonNull String condition) {
}

View File

@ -0,0 +1,75 @@
/*
* 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.api.block.custom;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.Map;
/**
* This class is used to store a custom block state, which contains CustomBlockData
* tied to defined properties and values
*/
public interface CustomBlockState {
/**
* Gets the custom block data associated with the state
*
* @return The custom block data for the state.
*/
@NonNull CustomBlockData block();
/**
* Gets the name of the state
*
* @return The name of the state.
*/
@NonNull String name();
/**
* Gets the given property for the state
*
* @param propertyName the property name
* @return the boolean, int, or string property.
*/
@NonNull <T> T property(@NonNull String propertyName);
/**
* Gets a map of the properties for the state
*
* @return The properties for the state.
*/
@NonNull Map<String, Object> properties();
interface Builder {
Builder booleanProperty(@NonNull String propertyName, boolean value);
Builder intProperty(@NonNull String propertyName, int value);
Builder stringProperty(@NonNull String propertyName, @NonNull String value);
CustomBlockState build();
}
}

View File

@ -0,0 +1,91 @@
/*
* Copyright (c) 2019-2023 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.api.block.custom;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.GeyserApi;
import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents;
import org.geysermc.geyser.api.util.CreativeCategory;
import java.util.List;
/**
* Represents a completely custom block that is not based on an existing vanilla Minecraft block.
*/
public interface NonVanillaCustomBlockData extends CustomBlockData {
/**
* Gets the namespace of the custom block
*
* @return The namespace of the custom block.
*/
@NonNull String namespace();
/**
* Create a Builder for NonVanillaCustomBlockData
*
* @return A NonVanillaCustomBlockData Builder
*/
static NonVanillaCustomBlockData.Builder builder() {
return GeyserApi.api().provider(NonVanillaCustomBlockData.Builder.class);
}
interface Builder extends CustomBlockData.Builder {
Builder namespace(@NonNull String namespace);
@Override
Builder name(@NonNull String name);
@Override
Builder includedInCreativeInventory(boolean includedInCreativeInventory);
@Override
Builder creativeCategory(@Nullable CreativeCategory creativeCategory);
@Override
Builder creativeGroup(@Nullable String creativeGroup);
@Override
Builder components(@NonNull CustomBlockComponents components);
@Override
Builder booleanProperty(@NonNull String propertyName);
@Override
Builder intProperty(@NonNull String propertyName, List<Integer> values);
@Override
Builder stringProperty(@NonNull String propertyName, List<String> values);
@Override
Builder permutations(@NonNull List<CustomBlockPermutation> permutations);
@Override
NonVanillaCustomBlockData build();
}
}

View File

@ -0,0 +1,69 @@
/*
* 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.api.block.custom.component;
/**
* This class is used to store a box component for the selection and
* collision boxes of a custom block.
*
* @param originX The origin X of the box
* @param originY The origin Y of the box
* @param originZ The origin Z of the box
* @param sizeX The size X of the box
* @param sizeY The size Y of the box
* @param sizeZ The size Z of the box
*/
public record BoxComponent(float originX, float originY, float originZ, float sizeX, float sizeY, float sizeZ) {
private static final BoxComponent FULL_BOX = new BoxComponent(-8, 0, -8, 16, 16, 16);
private static final BoxComponent EMPTY_BOX = new BoxComponent(0, 0, 0, 0, 0, 0);
/**
* Gets a full box component
*
* @return A full box component
*/
public static BoxComponent fullBox() {
return FULL_BOX;
}
/**
* Gets an empty box component
*
* @return An empty box component
*/
public static BoxComponent emptyBox() {
return EMPTY_BOX;
}
/**
* Gets if the box component is empty
*
* @return If the box component is empty.
*/
public boolean isEmpty() {
return sizeX == 0 && sizeY == 0 && sizeZ == 0;
}
}

View File

@ -0,0 +1,192 @@
/*
* 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.api.block.custom.component;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.GeyserApi;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* This class is used to store components for a custom block or custom block permutation.
*/
public interface CustomBlockComponents {
/**
* Gets the selection box component
* Equivalent to "minecraft:selection_box"
*
* @return The selection box.
*/
@Nullable BoxComponent selectionBox();
/**
* Gets the collision box component
* Equivalent to "minecraft:collision_box"
* @return The collision box.
*/
@Nullable BoxComponent collisionBox();
/**
* Gets the display name component
* Equivalent to "minecraft:display_name"
*
* @return The display name.
*/
@Nullable String displayName();
/**
* Gets the geometry component
* Equivalent to "minecraft:geometry"
*
* @return The geometry.
*/
@Nullable GeometryComponent geometry();
/**
* Gets the material instances component
* Equivalent to "minecraft:material_instances"
*
* @return The material instances.
*/
@NonNull Map<String, MaterialInstance> materialInstances();
/**
* Gets the placement filter component
* Equivalent to "minecraft:placement_filter"
*
* @return The placement filter.
*/
@Nullable List<PlacementConditions> placementFilter();
/**
* Gets the destructible by mining component
* Equivalent to "minecraft:destructible_by_mining"
*
* @return The destructible by mining value.
*/
@Nullable Float destructibleByMining();
/**
* Gets the friction component
* Equivalent to "minecraft:friction"
*
* @return The friction value.
*/
@Nullable Float friction();
/**
* Gets the light emission component
* Equivalent to "minecraft:light_emission"
*
* @return The light emission value.
*/
@Nullable Integer lightEmission();
/**
* Gets the light dampening component
* Equivalent to "minecraft:light_dampening"
*
* @return The light dampening value.
*/
@Nullable Integer lightDampening();
/**
* Gets the transformation component
* Equivalent to "minecraft:transformation"
*
* @return The transformation.
*/
@Nullable TransformationComponent transformation();
/**
* Gets the unit cube component
* Equivalent to "minecraft:unit_cube"
*
* @return The rotation.
*/
boolean unitCube();
/**
* Gets if the block should place only air
* Equivalent to setting a dummy event to run on "minecraft:on_player_placing"
*
* @return If the block should place only air.
*/
boolean placeAir();
/**
* Gets the set of tags
* Equivalent to "tag:some_tag"
*
* @return The set of tags.
*/
@NonNull Set<String> tags();
/**
* Create a Builder for CustomBlockComponents
*
* @return A CustomBlockComponents Builder
*/
static CustomBlockComponents.Builder builder() {
return GeyserApi.api().provider(CustomBlockComponents.Builder.class);
}
interface Builder {
Builder selectionBox(BoxComponent selectionBox);
Builder collisionBox(BoxComponent collisionBox);
Builder displayName(String displayName);
Builder geometry(GeometryComponent geometry);
Builder materialInstance(@NonNull String name, @NonNull MaterialInstance materialInstance);
Builder placementFilter(List<PlacementConditions> placementConditions);
Builder destructibleByMining(Float destructibleByMining);
Builder friction(Float friction);
Builder lightEmission(Integer lightEmission);
Builder lightDampening(Integer lightDampening);
Builder transformation(TransformationComponent transformation);
Builder unitCube(boolean unitCube);
Builder placeAir(boolean placeAir);
Builder tags(Set<String> tags);
CustomBlockComponents build();
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright (c) 2019-2023 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.api.block.custom.component;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.GeyserApi;
import java.util.Map;
/**
* This class is used to store data for a geometry component.
*/
public interface GeometryComponent {
/**
* Gets the identifier of the geometry
*
* @return The identifier of the geometry.
*/
@NonNull String identifier();
/**
* Gets the bone visibility of the geometry
*
* @return The bone visibility of the geometry.
*/
@Nullable Map<String, String> boneVisibility();
/**
* Creates a builder for GeometryComponent
*
* @return a builder for GeometryComponent.
*/
static GeometryComponent.Builder builder() {
return GeyserApi.api().provider(GeometryComponent.Builder.class);
}
interface Builder {
Builder identifier(@NonNull String identifier);
Builder boneVisibility(@Nullable Map<String, String> boneVisibility);
GeometryComponent build();
}
}

View File

@ -0,0 +1,84 @@
/*
* 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.api.block.custom.component;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.GeyserApi;
/**
* This class is used to store data for a material instance.
*/
public interface MaterialInstance {
/**
* Gets the texture of the block
*
* @return The texture of the block.
*/
@NonNull String texture();
/**
* Gets the render method of the block
*
* @return The render method of the block.
*/
@Nullable String renderMethod();
/**
* Gets if the block should be dimmed on certain faces
*
* @return If the block should be dimmed on certain faces.
*/
@Nullable boolean faceDimming();
/**
* Gets if the block should have ambient occlusion
*
* @return If the block should have ambient occlusion.
*/
@Nullable boolean ambientOcclusion();
/**
* Creates a builder for MaterialInstance.
*
* @return a builder for MaterialInstance
*/
static MaterialInstance.Builder builder() {
return GeyserApi.api().provider(MaterialInstance.Builder.class);
}
interface Builder {
Builder texture(@NonNull String texture);
Builder renderMethod(@Nullable String renderMethod);
Builder faceDimming(@Nullable boolean faceDimming);
Builder ambientOcclusion(@Nullable boolean ambientOcclusion);
MaterialInstance build();
}
}

View File

@ -0,0 +1,53 @@
/*
* 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.api.block.custom.component;
import java.util.LinkedHashMap;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* This class is used to store conditions for a placement filter for a custom block.
*
* @param allowedFaces The faces that the block can be placed on
* @param blockFilters The block filters that control what blocks the block can be placed on
*/
public record PlacementConditions(@NonNull Set<Face> allowedFaces, @NonNull LinkedHashMap<String, BlockFilterType> blockFilters) {
public enum Face {
DOWN,
UP,
NORTH,
SOUTH,
WEST,
EAST;
}
public enum BlockFilterType {
BLOCK,
TAG
}
}

View File

@ -0,0 +1,67 @@
/*
* 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.api.block.custom.component;
/**
* This class is used to store the transformation component of a block
*
* @param rx The rotation on the x axis
* @param ry The rotation on the y axis
* @param rz The rotation on the z axis
* @param sx The scale on the x axis
* @param sy The scale on the y axis
* @param sz The scale on the z axis
* @param tx The translation on the x axis
* @param ty The translation on the y axis
* @param tz The translation on the z axis
*/
public record TransformationComponent(int rx, int ry, int rz, float sx, float sy, float sz, float tx, float ty, float tz) {
/**
* Constructs a new TransformationComponent with the rotation values and assumes default scale and translation
*
* @param rx The rotation on the x axis
* @param ry The rotation on the y axis
* @param rz The rotation on the z axis
*/
public TransformationComponent(int rx, int ry, int rz) {
this(rx, ry, rz, 1, 1, 1, 0, 0, 0);
}
/**
* Constructs a new TransformationComponent with the rotation and scale values and assumes default translation
*
* @param rx The rotation on the x axis
* @param ry The rotation on the y axis
* @param rz The rotation on the z axis
* @param sx The scale on the x axis
* @param sy The scale on the y axis
* @param sz The scale on the z axis
*/
public TransformationComponent(int rx, int ry, int rz, float sx, float sy, float sz) {
this(rx, ry, rz, sx, sy, sz, 0, 0, 0);
}
}

View File

@ -0,0 +1,7 @@
package org.geysermc.geyser.api.block.custom.nonvanilla;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
public record JavaBlockItem(@NonNull String identifier, @NonNegative int javaId, @NonNegative int stackSize) {
}

View File

@ -0,0 +1,111 @@
package org.geysermc.geyser.api.block.custom.nonvanilla;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.GeyserApi;
public interface JavaBlockState {
/**
* Gets the identifier of the block state
*
* @return the identifier of the block state
*/
@NonNull String identifier();
/**
* Gets the Java ID of the block state
*
* @return the Java ID of the block state
*/
@NonNegative int javaId();
/**
* Gets the state group ID of the block state
*
* @return the state group ID of the block state
*/
@NonNegative int stateGroupId();
/**
* Gets the block hardness of the block state
*
* @return the block hardness of the block state
*/
@NonNegative float blockHardness();
/**
* Gets whether the block state is waterlogged
*
* @return whether the block state is waterlogged
*/
@NonNull boolean waterlogged();
/**
* Gets the collision of the block state
*
* @return the collision of the block state
*/
@NonNull JavaBoundingBox[] collision();
/**
* Gets whether the block state can be broken with hand
*
* @return whether the block state can be broken with hand
*/
@NonNull boolean canBreakWithHand();
/**
* Gets the pick item of the block state
*
* @return the pick item of the block state
*/
@Nullable String pickItem();
/**
* Gets the piston behavior of the block state
*
* @return the piston behavior of the block state
*/
@Nullable String pistonBehavior();
/**
* Gets whether the block state has block entity
*
* @return whether the block state has block entity
*/
@Nullable boolean hasBlockEntity();
/**
* Creates a new {@link JavaBlockState.Builder} instance
*
* @return a new {@link JavaBlockState.Builder} instance
*/
static JavaBlockState.Builder builder() {
return GeyserApi.api().provider(JavaBlockState.Builder.class);
}
interface Builder {
Builder identifier(@NonNull String identifier);
Builder javaId(@NonNegative int javaId);
Builder stateGroupId(@NonNegative int stateGroupId);
Builder blockHardness(@NonNegative float blockHardness);
Builder waterlogged(@NonNull boolean waterlogged);
Builder collision(@NonNull JavaBoundingBox[] collision);
Builder canBreakWithHand(@NonNull boolean canBreakWithHand);
Builder pickItem(@Nullable String pickItem);
Builder pistonBehavior(@Nullable String pistonBehavior);
Builder hasBlockEntity(@Nullable boolean hasBlockEntity);
JavaBlockState build();
}
}

View File

@ -0,0 +1,6 @@
package org.geysermc.geyser.api.block.custom.nonvanilla;
import org.checkerframework.checker.nullness.qual.NonNull;
public record JavaBoundingBox(@NonNull double middleX, @NonNull double middleY, @NonNull double middleZ, @NonNull double sizeX, @NonNull double sizeY, @NonNull double sizeZ) {
}

View File

@ -0,0 +1,56 @@
/*
* 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.api.block.custom.property;
import org.checkerframework.checker.nullness.qual.NonNull;
import java.util.List;
/**
* This class is used to store a property of a custom block of a generic type.
*/
public interface CustomBlockProperty<T> {
/**
* Gets the name of the property
*
* @return The name of the property.
*/
@NonNull String name();
/**
* Gets the values of the property
*
* @return The values of the property.
*/
@NonNull List<T> values();
/**
* Gets the type of the property
*
* @return The type of the property.
*/
@NonNull PropertyType type();
}

View File

@ -0,0 +1,79 @@
/*
* 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.api.block.custom.property;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* This class is used to define a custom block property's type.
*/
public class PropertyType {
private static final PropertyType BOOLEAN = new PropertyType(Boolean.class);
private static final PropertyType INTEGER = new PropertyType(Integer.class);
private static final PropertyType STRING = new PropertyType(String.class);
/**
* Gets the property type for a boolean.
*
* @return The property type for a boolean.
*/
@NonNull public static PropertyType booleanProp() {
return BOOLEAN;
}
/**
* Gets the property type for an integer.
*
* @return The property type for an integer.
*/
@NonNull public static PropertyType integerProp() {
return INTEGER;
}
/**
* Gets the property type for a string.
*
* @return The property type for a string.
*/
@NonNull public static PropertyType stringProp() {
return STRING;
}
private final Class<?> typeClass;
/**
* Gets the class of the property type
*
* @return The class of the property type.
*/
@NonNull public Class<?> typeClass() {
return typeClass;
}
private PropertyType(Class<?> typeClass) {
this.typeClass = typeClass;
}
}

View File

@ -0,0 +1,76 @@
/*
* 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.api.event.lifecycle;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.block.custom.CustomBlockData;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockItem;
import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState;
import org.geysermc.event.Event;
/**
* Called on Geyser's startup when looking for custom blocks. Custom blocks must be registered through this event.
*
* This event will not be called if the "add-non-bedrock-items" setting is disabled in the Geyser config.
*/
public abstract class GeyserDefineCustomBlocksEvent implements Event {
/**
* Registers the given {@link CustomBlockData} as a custom block
*
* @param customBlockData the custom block to register
*/
public abstract void register(@NonNull CustomBlockData customBlockData);
/**
* Registers the given {@link CustomBlockState} as an override for the
* given java state identifier
* Java state identifiers are listed in
* https://raw.githubusercontent.com/GeyserMC/mappings/master/blocks.json
*
* @param javaIdentifier the java state identifier to override
* @param customBlockState the custom block state with which to override java state identifier
*/
public abstract void registerOverride(@NonNull String javaIdentifier, @NonNull CustomBlockState customBlockState);
/**
* Registers the given {@link CustomBlockData} as an override for the
* given java item identifier
*
* @param javaIdentifier the java item identifier to override
* @param customBlockData the custom block data with which to override java item identifier
*/
public abstract void registerItemOverride(@NonNull String javaIdentifier, @NonNull CustomBlockData customBlockData);
/**
* Registers the given {@link CustomBlockState} as an override for the
* given {@link JavaBlockState}
*
* @param javaBlockState the java block state for the non-vanilla block
* @param customBlockState the custom block state with which to override java state identifier
*/
public abstract void registerOverride(@NonNull JavaBlockState javaBlockState, @NonNull CustomBlockState customBlockState);
}

View File

@ -0,0 +1,28 @@
package org.geysermc.geyser.api.event.lifecycle;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.event.Event;
/**
* Called on Geyser's startup when looking for custom skulls. Custom skulls must be registered through this event.
*
* This event will not be called if the "add-non-bedrock-items" setting is disabled in the Geyser config.
*/
public abstract class GeyserDefineCustomSkullsEvent implements Event {
/**
* The type of texture provided
*/
public enum SkullTextureType {
USERNAME,
UUID,
PROFILE,
SKIN_HASH
}
/**
* Registers the given username, UUID, base64 encoded profile, or skin hash as a custom skull blocks
* @param texture the username, UUID, base64 encoded profile, or skin hash
* @param type the type of texture provided
*/
public abstract void register(@NonNull String texture, @NonNull SkullTextureType type);
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) 2019-2023 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.api.util;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* Represents the creative menu categories or tabs.
*/
public enum CreativeCategory {
COMMANDS("commands", 1),
CONSTRUCTION("construction", 2),
EQUIPMENT("equipment", 3),
ITEMS("items", 4),
NATURE("nature", 5),
NONE("none", 6);
private final String internalName;
private final int id;
CreativeCategory(String internalName, int id) {
this.internalName = internalName;
this.id = id;
}
/**
* Gets the internal name of the category.
*
* @return the name of the category
*/
@NonNull public String internalName() {
return internalName;
}
/**
* Gets the internal ID of the category.
*
* @return the ID of the category
*/
public int id() {
return id;
}
}

View File

@ -8,7 +8,7 @@ repositories {
}
dependencies {
implementation("net.kyori", "indra-common", "3.0.1")
implementation("net.kyori", "indra-common", "3.1.1")
implementation("com.github.johnrengelman", "shadow", "7.1.3-SNAPSHOT")
// Within the gradle plugin classpath, there is a version conflict between loom and some other

View File

@ -34,4 +34,4 @@ tasks {
)
}
}
}
}

View File

@ -5,9 +5,15 @@ plugins {
}
allprojects {
group = "org.geysermc.geyser"
version = "2.1.2-SNAPSHOT"
description = "Allows for players from Minecraft: Bedrock Edition to join Minecraft: Java Edition servers."
group = properties["group"] as String + "." + properties["id"] as String
version = properties["version"] as String
description = properties["description"] as String
}
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(16))
}
}
val platforms = setOf(

View File

@ -41,6 +41,10 @@ public final class Constants {
static final String SAVED_REFRESH_TOKEN_FILE = "saved-refresh-tokens.json";
public static final String GEYSER_CUSTOM_NAMESPACE = "geyser_custom";
public static final String MINECRAFT_SKIN_SERVER_URL = "https://textures.minecraft.net/texture/";
static {
URI wsUri = null;
try {

View File

@ -0,0 +1,65 @@
/*
* 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.configuration;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@JsonIgnoreProperties(ignoreUnknown = true)
@SuppressWarnings("FieldMayBeFinal") // Jackson requires that the fields are not final
public class GeyserCustomSkullConfiguration {
@JsonProperty("player-usernames")
private List<String> playerUsernames;
@JsonProperty("player-uuids")
private List<String> playerUUIDs;
@JsonProperty("player-profiles")
private List<String> playerProfiles;
@JsonProperty("skin-hashes")
private List<String> skinHashes;
public List<String> getPlayerUsernames() {
return Objects.requireNonNullElse(playerUsernames, Collections.emptyList());
}
public List<String> getPlayerUUIDs() {
return Objects.requireNonNullElse(playerUUIDs, Collections.emptyList());
}
public List<String> getPlayerProfiles() {
return Objects.requireNonNullElse(playerProfiles, Collections.emptyList());
}
public List<String> getPlayerSkinHashes() {
return Objects.requireNonNullElse(skinHashes, Collections.emptyList());
}
}

View File

@ -77,15 +77,18 @@ public class BlockInventoryHolder extends InventoryHolder {
// (This could be a virtual inventory that the player is opening)
if (checkInteractionPosition(session)) {
// Then, check to see if the interacted block is valid for this inventory by ensuring the block state identifier is valid
// and the bedrock block is vanilla
int javaBlockId = session.getGeyser().getWorldManager().getBlockAt(session, session.getLastInteractionBlockPosition());
String[] javaBlockString = BlockRegistries.JAVA_BLOCKS.getOrDefault(javaBlockId, BlockMapping.AIR).getJavaIdentifier().split("\\[");
if (isValidBlock(javaBlockString)) {
// We can safely use this block
inventory.setHolderPosition(session.getLastInteractionBlockPosition());
((Container) inventory).setUsingRealBlock(true, javaBlockString[0]);
setCustomName(session, session.getLastInteractionBlockPosition(), inventory, javaBlockId);
if (!BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get().containsKey(javaBlockId)) {
String[] javaBlockString = BlockRegistries.JAVA_BLOCKS.getOrDefault(javaBlockId, BlockMapping.AIR).getJavaIdentifier().split("\\[");
if (isValidBlock(javaBlockString)) {
// We can safely use this block
inventory.setHolderPosition(session.getLastInteractionBlockPosition());
((Container) inventory).setUsingRealBlock(true, javaBlockString[0]);
setCustomName(session, session.getLastInteractionBlockPosition(), inventory, javaBlockId);
return true;
return true;
}
}
}
@ -97,7 +100,7 @@ public class BlockInventoryHolder extends InventoryHolder {
UpdateBlockPacket blockPacket = new UpdateBlockPacket();
blockPacket.setDataLayer(0);
blockPacket.setBlockPosition(position);
blockPacket.setDefinition(session.getBlockMappings().getBedrockBlock(defaultJavaBlockState));
blockPacket.setDefinition(session.getBlockMappings().getVanillaBedrockBlock(defaultJavaBlockState));
blockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY);
session.sendUpstreamPacket(blockPacket);
inventory.setHolderPosition(position);

View File

@ -1,132 +0,0 @@
/*
* 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.item.mappings.versions;
import com.fasterxml.jackson.databind.JsonNode;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.item.custom.CustomItemData;
import org.geysermc.geyser.api.item.custom.CustomItemOptions;
import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException;
import java.nio.file.Path;
import java.util.function.BiConsumer;
public class MappingsReader_v1 extends MappingsReader {
@Override
public void readMappings(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomItemData> consumer) {
this.readItemMappings(file, mappingsRoot, consumer);
}
public void readItemMappings(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomItemData> consumer) {
JsonNode itemsNode = mappingsRoot.get("items");
if (itemsNode != null && itemsNode.isObject()) {
itemsNode.fields().forEachRemaining(entry -> {
if (entry.getValue().isArray()) {
entry.getValue().forEach(data -> {
try {
CustomItemData customItemData = this.readItemMappingEntry(data);
consumer.accept(entry.getKey(), customItemData);
} catch (InvalidCustomMappingsFileException e) {
GeyserImpl.getInstance().getLogger().error("Error in custom mapping file: " + file.toString(), e);
}
});
}
});
}
}
private CustomItemOptions readItemCustomItemOptions(JsonNode node) {
CustomItemOptions.Builder customItemOptions = CustomItemOptions.builder();
JsonNode customModelData = node.get("custom_model_data");
if (customModelData != null && customModelData.isInt()) {
customItemOptions.customModelData(customModelData.asInt());
}
JsonNode damagePredicate = node.get("damage_predicate");
if (damagePredicate != null && damagePredicate.isInt()) {
customItemOptions.damagePredicate(damagePredicate.asInt());
}
JsonNode unbreakable = node.get("unbreakable");
if (unbreakable != null && unbreakable.isBoolean()) {
customItemOptions.unbreakable(unbreakable.asBoolean());
}
JsonNode defaultItem = node.get("default");
if (defaultItem != null && defaultItem.isBoolean()) {
customItemOptions.defaultItem(defaultItem.asBoolean());
}
return customItemOptions.build();
}
@Override
public CustomItemData readItemMappingEntry(JsonNode node) throws InvalidCustomMappingsFileException {
if (node == null || !node.isObject()) {
throw new InvalidCustomMappingsFileException("Invalid item mappings entry");
}
JsonNode name = node.get("name");
if (name == null || !name.isTextual() || name.asText().isEmpty()) {
throw new InvalidCustomMappingsFileException("An item entry has no name");
}
CustomItemData.Builder customItemData = CustomItemData.builder()
.name(name.asText())
.customItemOptions(this.readItemCustomItemOptions(node));
//The next entries are optional
if (node.has("display_name")) {
customItemData.displayName(node.get("display_name").asText());
}
if (node.has("icon")) {
customItemData.icon(node.get("icon").asText());
}
if (node.has("allow_offhand")) {
customItemData.allowOffhand(node.get("allow_offhand").asBoolean());
}
if (node.has("display_handheld")) {
customItemData.displayHandheld(node.get("display_handheld").asBoolean());
}
if (node.has("texture_size")) {
customItemData.textureSize(node.get("texture_size").asInt());
}
if (node.has("render_offsets")) {
JsonNode tmpNode = node.get("render_offsets");
customItemData.renderOffsets(fromJsonNode(tmpNode));
}
return customItemData.build();
}
}

View File

@ -32,6 +32,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.geyser.translator.text.MessageTranslator;
public class PlayerHeadItem extends Item {
public PlayerHeadItem(String javaIdentifier, Builder builder) {
@ -42,29 +43,35 @@ public class PlayerHeadItem extends Item {
public void translateNbtToBedrock(@NonNull GeyserSession session, @NonNull CompoundTag tag) {
super.translateNbtToBedrock(session, tag);
Tag display = tag.get("display");
if (!(display instanceof CompoundTag) || !((CompoundTag) display).contains("Name")) {
Tag skullOwner = tag.get("SkullOwner");
if (skullOwner != null) {
CompoundTag displayTag;
if (tag.get("display") instanceof CompoundTag existingDisplayTag) {
displayTag = existingDisplayTag;
} else {
displayTag = new CompoundTag("display");
tag.put(displayTag);
}
if (displayTag.get("Name") instanceof StringTag nameTag) {
// Custom names are always yellow and italic
displayTag.put(new StringTag("Name", ChatColor.YELLOW + ChatColor.ITALIC + MessageTranslator.convertMessageLenient(nameTag.getValue(), session.locale())));
} else {
if (tag.contains("SkullOwner")) {
StringTag name;
if (skullOwner instanceof StringTag) {
name = (StringTag) skullOwner;
Tag skullOwner = tag.get("SkullOwner");
if (skullOwner instanceof StringTag skullName) {
name = skullName;
} else {
StringTag skullName;
if (skullOwner instanceof CompoundTag && (skullName = ((CompoundTag) skullOwner).get("Name")) != null) {
if (skullOwner instanceof CompoundTag && ((CompoundTag) skullOwner).get("Name") instanceof StringTag skullName) {
name = skullName;
} else {
session.getGeyser().getLogger().debug("Not sure how to handle skull head item display. " + tag);
// No name found so default to "Player Head"
displayTag.put(new StringTag("Name", ChatColor.RESET + ChatColor.YELLOW + MinecraftLocale.getLocaleString("block.minecraft.player_head", session.locale())));
return;
}
}
// Add correct name of player skull
// TODO: It's always yellow, even with a custom name. Handle?
String displayName = ChatColor.RESET + ChatColor.YELLOW + MinecraftLocale.getLocaleString("block.minecraft.player_head.named", session.locale()).replace("%s", name.getValue());
if (!(display instanceof CompoundTag)) {
tag.put(display = new CompoundTag("display"));
}
((CompoundTag) display).put(new StringTag("Name", displayName));
displayTag.put(new StringTag("Name", displayName));
}
}
}

View File

@ -0,0 +1,305 @@
/*
* 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.level.block;
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.Value;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.block.custom.component.BoxComponent;
import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents;
import org.geysermc.geyser.api.block.custom.component.GeometryComponent;
import org.geysermc.geyser.api.block.custom.component.MaterialInstance;
import org.geysermc.geyser.api.block.custom.component.PlacementConditions;
import org.geysermc.geyser.api.block.custom.component.TransformationComponent;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Value
public class GeyserCustomBlockComponents implements CustomBlockComponents {
BoxComponent selectionBox;
BoxComponent collisionBox;
String displayName;
GeometryComponent geometry;
Map<String, MaterialInstance> materialInstances;
List<PlacementConditions> placementFilter;
Float destructibleByMining;
Float friction;
Integer lightEmission;
Integer lightDampening;
TransformationComponent transformation;
boolean unitCube;
boolean placeAir;
Set<String> tags;
private GeyserCustomBlockComponents(CustomBlockComponentsBuilder builder) {
this.selectionBox = builder.selectionBox;
this.collisionBox = builder.collisionBox;
this.displayName = builder.displayName;
this.geometry = builder.geometry;
if (builder.materialInstances.isEmpty()) {
this.materialInstances = Object2ObjectMaps.emptyMap();
} else {
this.materialInstances = Object2ObjectMaps.unmodifiable(new Object2ObjectArrayMap<>(builder.materialInstances));
}
this.placementFilter = builder.placementFilter;
this.destructibleByMining = builder.destructibleByMining;
this.friction = builder.friction;
this.lightEmission = builder.lightEmission;
this.lightDampening = builder.lightDampening;
this.transformation = builder.transformation;
this.unitCube = builder.unitCube;
this.placeAir = builder.placeAir;
if (builder.tags.isEmpty()) {
this.tags = Set.of();
} else {
this.tags = Set.copyOf(builder.tags);
}
}
@Override
public BoxComponent selectionBox() {
return selectionBox;
}
@Override
public BoxComponent collisionBox() {
return collisionBox;
}
@Override
public String displayName() {
return displayName;
}
@Override
public GeometryComponent geometry() {
return geometry;
}
@Override
public @NonNull Map<String, MaterialInstance> materialInstances() {
return materialInstances;
}
@Override
public List<PlacementConditions> placementFilter() {
return placementFilter;
}
@Override
public Float destructibleByMining() {
return destructibleByMining;
}
@Override
public Float friction() {
return friction;
}
@Override
public Integer lightEmission() {
return lightEmission;
}
@Override
public Integer lightDampening() {
return lightDampening;
}
@Override
public TransformationComponent transformation() {
return transformation;
}
@Override
public boolean unitCube() {
return unitCube;
}
@Override
public boolean placeAir() {
return placeAir;
}
@Override
public @NotNull Set<String> tags() {
return tags;
}
public static class CustomBlockComponentsBuilder implements Builder {
protected BoxComponent selectionBox;
protected BoxComponent collisionBox;
protected String displayName;
protected GeometryComponent geometry;
protected final Object2ObjectMap<String, MaterialInstance> materialInstances = new Object2ObjectOpenHashMap<>();
protected List<PlacementConditions> placementFilter;
protected Float destructibleByMining;
protected Float friction;
protected Integer lightEmission;
protected Integer lightDampening;
protected TransformationComponent transformation;
protected boolean unitCube = false;
protected boolean placeAir = false;
protected final Set<String> tags = new HashSet<>();
private void validateBox(BoxComponent box) {
if (box == null) {
return;
}
if (box.sizeX() < 0 || box.sizeY() < 0 || box.sizeZ() < 0) {
throw new IllegalArgumentException("Box size must be non-negative.");
}
float minX = box.originX() + 8;
float minY = box.originY();
float minZ = box.originZ() + 8;
float maxX = minX + box.sizeX();
float maxY = minY + box.sizeY();
float maxZ = minZ + box.sizeZ();
if (minX < 0 || minY < 0 || minZ < 0 || maxX > 16 || maxY > 16 || maxZ > 16) {
throw new IllegalArgumentException("Box bounds must be within (0, 0, 0) and (16, 16, 16). Recieved: (" + minX + ", " + minY + ", " + minZ + ") to (" + maxX + ", " + maxY + ", " + maxZ + ")");
}
}
@Override
public Builder selectionBox(BoxComponent selectionBox) {
validateBox(selectionBox);
this.selectionBox = selectionBox;
return this;
}
@Override
public Builder collisionBox(BoxComponent collisionBox) {
validateBox(collisionBox);
this.collisionBox = collisionBox;
return this;
}
@Override
public Builder displayName(String displayName) {
this.displayName = displayName;
return this;
}
@Override
public Builder geometry(GeometryComponent geometry) {
this.geometry = geometry;
return this;
}
@Override
public Builder materialInstance(@NotNull String name, @NotNull MaterialInstance materialInstance) {
this.materialInstances.put(name, materialInstance);
return this;
}
@Override
public Builder placementFilter(List<PlacementConditions> placementFilter) {
this.placementFilter = placementFilter;
return this;
}
@Override
public Builder destructibleByMining(Float destructibleByMining) {
if (destructibleByMining != null && destructibleByMining < 0) {
throw new IllegalArgumentException("Destructible by mining must be non-negative");
}
this.destructibleByMining = destructibleByMining;
return this;
}
@Override
public Builder friction(Float friction) {
if (friction != null) {
if (friction < 0 || friction > 1) {
throw new IllegalArgumentException("Friction must be in the range 0-1");
}
}
this.friction = friction;
return this;
}
@Override
public Builder lightEmission(Integer lightEmission) {
if (lightEmission != null) {
if (lightEmission < 0 || lightEmission > 15) {
throw new IllegalArgumentException("Light emission must be in the range 0-15");
}
}
this.lightEmission = lightEmission;
return this;
}
@Override
public Builder lightDampening(Integer lightDampening) {
if (lightDampening != null) {
if (lightDampening < 0 || lightDampening > 15) {
throw new IllegalArgumentException("Light dampening must be in the range 0-15");
}
}
this.lightDampening = lightDampening;
return this;
}
@Override
public Builder transformation(TransformationComponent transformation) {
if (transformation.rx() % 90 != 0 || transformation.ry() % 90 != 0 || transformation.rz() % 90 != 0) {
throw new IllegalArgumentException("Rotation of transformation must be a multiple of 90 degrees.");
}
this.transformation = transformation;
return this;
}
@Override
public Builder unitCube(boolean unitCube) {
this.unitCube = unitCube;
return this;
}
@Override
public Builder placeAir(boolean placeAir) {
this.placeAir = placeAir;
return this;
}
@Override
public Builder tags(Set<String> tags) {
this.tags.addAll(tags);
return this;
}
@Override
public CustomBlockComponents build() {
return new GeyserCustomBlockComponents(this);
}
}
}

View File

@ -0,0 +1,217 @@
/*
* 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.level.block;
import it.unimi.dsi.fastutil.objects.*;
import lombok.RequiredArgsConstructor;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.block.custom.CustomBlockData;
import org.geysermc.geyser.api.block.custom.CustomBlockPermutation;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents;
import org.geysermc.geyser.api.block.custom.property.CustomBlockProperty;
import org.geysermc.geyser.api.block.custom.property.PropertyType;
import org.geysermc.geyser.api.util.CreativeCategory;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import java.util.List;
import java.util.Map;
@RequiredArgsConstructor
public class GeyserCustomBlockData implements CustomBlockData {
private final String name;
private final boolean includedInCreativeInventory;
private final CreativeCategory creativeCategory;
private final String creativeGroup;
private final CustomBlockComponents components;
private final Map<String, CustomBlockProperty<?>> properties;
private final List<CustomBlockPermutation> permutations;
private final Map<String, Object> defaultProperties;
GeyserCustomBlockData(CustomBlockDataBuilder builder) {
this.name = builder.name;
if (name == null) {
throw new IllegalStateException("Name must be set");
}
this.includedInCreativeInventory = builder.includedInCreativeInventory;
this.creativeCategory = builder.creativeCategory;
this.creativeGroup = builder.creativeGroup;
this.components = builder.components;
if (!builder.properties.isEmpty()) {
this.properties = Object2ObjectMaps.unmodifiable(new Object2ObjectArrayMap<>(builder.properties));
Object2ObjectMap<String, Object> defaultProperties = new Object2ObjectOpenHashMap<>(this.properties.size());
for (CustomBlockProperty<?> property : properties.values()) {
if (property.values().size() > 16) {
GeyserImpl.getInstance().getLogger().warning(property.name() + " contains more than 16 values, but BDS specifies it should not. This may break in future versions.");
}
if (property.values().stream().distinct().count() != property.values().size()) {
throw new IllegalStateException(property.name() + " has duplicate values.");
}
if (property.values().isEmpty()) {
throw new IllegalStateException(property.name() + " contains no values.");
}
defaultProperties.put(property.name(), property.values().get(0));
}
this.defaultProperties = Object2ObjectMaps.unmodifiable(defaultProperties);
} else {
this.properties = Object2ObjectMaps.emptyMap();
this.defaultProperties = Object2ObjectMaps.emptyMap();
}
if (!builder.permutations.isEmpty()) {
this.permutations = List.of(builder.permutations.toArray(new CustomBlockPermutation[0]));
} else {
this.permutations = ObjectLists.emptyList();
}
}
@Override
public @NonNull String name() {
return name;
}
@Override
public @NonNull String identifier() {
return Constants.GEYSER_CUSTOM_NAMESPACE + ":" + name;
}
@Override
public boolean includedInCreativeInventory() {
return includedInCreativeInventory;
}
@Override
public @Nullable CreativeCategory creativeCategory() {
return creativeCategory;
}
@Override
public @Nullable String creativeGroup() {
return creativeGroup;
}
@Override
public CustomBlockComponents components() {
return components;
}
@Override
public @NonNull Map<String, CustomBlockProperty<?>> properties() {
return properties;
}
@Override
public @NonNull List<CustomBlockPermutation> permutations() {
return permutations;
}
@Override
public @NonNull CustomBlockState defaultBlockState() {
return new GeyserCustomBlockState(this, defaultProperties);
}
@Override
public CustomBlockState.@NotNull Builder blockStateBuilder() {
return new GeyserCustomBlockState.CustomBlockStateBuilder(this);
}
public static class CustomBlockDataBuilder implements Builder {
private String name;
private boolean includedInCreativeInventory;
private CreativeCategory creativeCategory;
private String creativeGroup;
private CustomBlockComponents components;
private final Object2ObjectMap<String, CustomBlockProperty<?>> properties = new Object2ObjectOpenHashMap<>();
private List<CustomBlockPermutation> permutations = ObjectLists.emptyList();
@Override
public Builder name(@NonNull String name) {
this.name = name;
return this;
}
@Override
public Builder includedInCreativeInventory(boolean includedInCreativeInventory) {
this.includedInCreativeInventory = includedInCreativeInventory;
return this;
}
@Override
public Builder creativeCategory(@Nullable CreativeCategory creativeCategory) {
this.creativeCategory = creativeCategory;
return this;
}
@Override
public Builder creativeGroup(@Nullable String creativeGroup) {
this.creativeGroup = creativeGroup;
return this;
}
@Override
public Builder components(@NonNull CustomBlockComponents components) {
this.components = components;
return this;
}
@Override
public Builder booleanProperty(@NonNull String propertyName) {
this.properties.put(propertyName, new GeyserCustomBlockProperty<>(propertyName, List.of((byte) 0, (byte) 1), PropertyType.booleanProp()));
return this;
}
@Override
public Builder intProperty(@NonNull String propertyName, List<Integer> values) {
this.properties.put(propertyName, new GeyserCustomBlockProperty<>(propertyName, values, PropertyType.integerProp()));
return this;
}
@Override
public Builder stringProperty(@NonNull String propertyName, List<String> values) {
this.properties.put(propertyName, new GeyserCustomBlockProperty<>(propertyName, values, PropertyType.stringProp()));
return this;
}
@Override
public Builder permutations(@NonNull List<CustomBlockPermutation> permutations) {
this.permutations = permutations;
return this;
}
@Override
public CustomBlockData build() {
return new GeyserCustomBlockData(this);
}
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.level.block;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.block.custom.property.CustomBlockProperty;
import org.geysermc.geyser.api.block.custom.property.PropertyType;
import java.util.List;
/**
* A custom block property that can be used to store custom data for a block.
*
* @param <T> The type of the property
* @param name The name of the property
* @param values The values of the property
* @param type The type of the property
*/
public record GeyserCustomBlockProperty<T>(@NonNull String name, @NonNull List<T> values,
@NonNull PropertyType type) implements CustomBlockProperty<T> {
}

View File

@ -0,0 +1,112 @@
/*
* 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.level.block;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.block.custom.CustomBlockData;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.api.block.custom.property.CustomBlockProperty;
import java.util.Map;
@Value
public class GeyserCustomBlockState implements CustomBlockState {
CustomBlockData block;
Map<String, Object> properties;
@Override
public @NonNull CustomBlockData block() {
return block;
}
@Override
public @NonNull String name() {
return block.name();
}
@SuppressWarnings("unchecked")
@Override
public <T> @NonNull T property(String propertyName) {
return (T) properties.get(propertyName);
}
@Override
public @NonNull Map<String, Object> properties() {
return properties;
}
@RequiredArgsConstructor
public static class CustomBlockStateBuilder implements CustomBlockState.Builder {
private final CustomBlockData blockData;
private final Object2ObjectMap<String, Object> properties = new Object2ObjectOpenHashMap<>();
@Override
public Builder booleanProperty(@NonNull String propertyName, boolean value) {
properties.put(propertyName, value ? (byte) 1 : (byte) 0);
return this;
}
@Override
public Builder intProperty(@NonNull String propertyName, int value) {
properties.put(propertyName, value);
return this;
}
@Override
public Builder stringProperty(@NonNull String propertyName, @NonNull String value) {
properties.put(propertyName, value);
return this;
}
@Override
public CustomBlockState build() {
for (String propertyName : blockData.properties().keySet()) {
if (!properties.containsKey(propertyName)) {
throw new IllegalArgumentException("Missing property: " + propertyName);
}
}
for (Map.Entry<String, Object> entry : properties.entrySet()) {
String propertyName = entry.getKey();
Object propertyValue = entry.getValue();
CustomBlockProperty<?> property = blockData.properties().get(propertyName);
if (property == null) {
throw new IllegalArgumentException("Unknown property: " + propertyName);
} else if (!property.values().contains(propertyValue)) {
throw new IllegalArgumentException("Invalid value: " + propertyValue + " for property: " + propertyName);
}
}
return new GeyserCustomBlockState(blockData, Object2ObjectMaps.unmodifiable(properties));
}
}
}

View File

@ -0,0 +1,76 @@
/*
* Copyright (c) 2019-2023 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.level.block;
import lombok.RequiredArgsConstructor;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.block.custom.component.GeometryComponent;
import java.util.Map;
@RequiredArgsConstructor
public class GeyserGeometryComponent implements GeometryComponent {
private final String identifier;
private final Map<String, String> boneVisibility;
GeyserGeometryComponent(GeometryComponentBuilder builder) {
this.identifier = builder.identifier;
this.boneVisibility = builder.boneVisibility;
}
@Override
public @NonNull String identifier() {
return identifier;
}
@Override
public @Nullable Map<String, String> boneVisibility() {
return boneVisibility;
}
public static class GeometryComponentBuilder implements Builder {
private String identifier;
private Map<String, String> boneVisibility;
@Override
public GeometryComponent.Builder identifier(@NonNull String identifier) {
this.identifier = identifier;
return this;
}
@Override
public GeometryComponent.Builder boneVisibility(@Nullable Map<String, String> boneVisibility) {
this.boneVisibility = boneVisibility;
return this;
}
@Override
public GeometryComponent build() {
return new GeyserGeometryComponent(this);
}
}
}

View File

@ -0,0 +1,161 @@
package org.geysermc.geyser.level.block;
import org.checkerframework.checker.index.qual.NonNegative;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState;
import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBoundingBox;
public class GeyserJavaBlockState implements JavaBlockState {
String identifier;
int javaId;
int stateGroupId;
float blockHardness;
boolean waterlogged;
JavaBoundingBox[] collision;
boolean canBreakWithHand;
String pickItem;
String pistonBehavior;
boolean hasBlockEntity;
private GeyserJavaBlockState(JavaBlockStateBuilder builder) {
this.identifier = builder.identifier;
this.javaId = builder.javaId;
this.stateGroupId = builder.stateGroupId;
this.blockHardness = builder.blockHardness;
this.waterlogged = builder.waterlogged;
this.collision = builder.collision;
this.canBreakWithHand = builder.canBreakWithHand;
this.pickItem = builder.pickItem;
this.pistonBehavior = builder.pistonBehavior;
this.hasBlockEntity = builder.hasBlockEntity;
}
@Override
public @NonNull String identifier() {
return identifier;
}
@Override
public @NonNegative int javaId() {
return javaId;
}
@Override
public @NonNegative int stateGroupId() {
return stateGroupId;
}
@Override
public @NonNegative float blockHardness() {
return blockHardness;
}
@Override
public @NonNull boolean waterlogged() {
return waterlogged;
}
@Override
public @NonNull JavaBoundingBox[] collision() {
return collision;
}
@Override
public @NonNull boolean canBreakWithHand() {
return canBreakWithHand;
}
@Override
public @Nullable String pickItem() {
return pickItem;
}
@Override
public @Nullable String pistonBehavior() {
return pistonBehavior;
}
@Override
public @Nullable boolean hasBlockEntity() {
return hasBlockEntity;
}
public static class JavaBlockStateBuilder implements Builder {
private String identifier;
private int javaId;
private int stateGroupId;
private float blockHardness;
private boolean waterlogged;
private JavaBoundingBox[] collision;
private boolean canBreakWithHand;
private String pickItem;
private String pistonBehavior;
private boolean hasBlockEntity;
@Override
public Builder identifier(@NonNull String identifier) {
this.identifier = identifier;
return this;
}
@Override
public Builder javaId(@NonNegative int javaId) {
this.javaId = javaId;
return this;
}
@Override
public Builder stateGroupId(@NonNegative int stateGroupId) {
this.stateGroupId = stateGroupId;
return this;
}
@Override
public Builder blockHardness(@NonNegative float blockHardness) {
this.blockHardness = blockHardness;
return this;
}
@Override
public Builder waterlogged(@NonNull boolean waterlogged) {
this.waterlogged = waterlogged;
return this;
}
@Override
public Builder collision(@NonNull JavaBoundingBox[] collision) {
this.collision = collision;
return this;
}
@Override
public Builder canBreakWithHand(@NonNull boolean canBreakWithHand) {
this.canBreakWithHand = canBreakWithHand;
return this;
}
@Override
public Builder pickItem(@Nullable String pickItem) {
this.pickItem = pickItem;
return this;
}
@Override
public Builder pistonBehavior(@Nullable String pistonBehavior) {
this.pistonBehavior = pistonBehavior;
return this;
}
@Override
public Builder hasBlockEntity(@Nullable boolean hasBlockEntity) {
this.hasBlockEntity = hasBlockEntity;
return this;
}
@Override
public JavaBlockState build() {
return new GeyserJavaBlockState(this);
}
}
}

View File

@ -0,0 +1,102 @@
/*
* Copyright (c) 2019-2023 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.level.block;
import lombok.RequiredArgsConstructor;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.block.custom.component.MaterialInstance;
@RequiredArgsConstructor
public class GeyserMaterialInstance implements MaterialInstance {
private final String texture;
private final String renderMethod;
private final boolean faceDimming;
private final boolean ambientOcclusion;
GeyserMaterialInstance(MaterialInstanceBuilder builder) {
this.texture = builder.texture;
this.renderMethod = builder.renderMethod;
this.faceDimming = builder.faceDimming;
this.ambientOcclusion = builder.ambientOcclusion;
}
@Override
public @NonNull String texture() {
return texture;
}
@Override
public @Nullable String renderMethod() {
return renderMethod;
}
@Override
public @Nullable boolean faceDimming() {
return faceDimming;
}
@Override
public @Nullable boolean ambientOcclusion() {
return ambientOcclusion;
}
public static class MaterialInstanceBuilder implements Builder {
private String texture;
private String renderMethod;
private boolean faceDimming;
private boolean ambientOcclusion;
@Override
public Builder texture(@NonNull String texture) {
this.texture = texture;
return this;
}
@Override
public Builder renderMethod(@Nullable String renderMethod) {
this.renderMethod = renderMethod;
return this;
}
@Override
public Builder faceDimming(@Nullable boolean faceDimming) {
this.faceDimming = faceDimming;
return this;
}
@Override
public Builder ambientOcclusion(@Nullable boolean ambientOcclusion) {
this.ambientOcclusion = ambientOcclusion;
return this;
}
@Override
public MaterialInstance build() {
return new GeyserMaterialInstance(this);
}
}
}

View File

@ -0,0 +1,118 @@
/*
* Copyright (c) 2019-2023 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.level.block;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.geysermc.geyser.api.block.custom.CustomBlockPermutation;
import org.geysermc.geyser.api.block.custom.NonVanillaCustomBlockData;
import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents;
import org.geysermc.geyser.api.util.CreativeCategory;
import java.util.List;
public class GeyserNonVanillaCustomBlockData extends GeyserCustomBlockData implements NonVanillaCustomBlockData {
private final String namespace;
GeyserNonVanillaCustomBlockData(NonVanillaCustomBlockDataBuilder builder) {
super(builder);
this.namespace = builder.namespace;
if (namespace == null) {
throw new IllegalStateException("Identifier must be set");
}
}
@Override
public @NonNull String identifier() {
return this.namespace + ":" + super.name();
}
@Override
public @NonNull String namespace() {
return this.namespace;
}
public static class NonVanillaCustomBlockDataBuilder extends CustomBlockDataBuilder implements NonVanillaCustomBlockData.Builder {
private String namespace;
@Override
public NonVanillaCustomBlockDataBuilder namespace(@NonNull String namespace) {
this.namespace = namespace;
return this;
}
@Override
public NonVanillaCustomBlockDataBuilder name(@NonNull String name) {
return (NonVanillaCustomBlockDataBuilder) super.name(name);
}
@Override
public NonVanillaCustomBlockDataBuilder includedInCreativeInventory(boolean includedInCreativeInventory) {
return (NonVanillaCustomBlockDataBuilder) super.includedInCreativeInventory(includedInCreativeInventory);
}
@Override
public NonVanillaCustomBlockDataBuilder creativeCategory(@Nullable CreativeCategory creativeCategories) {
return (NonVanillaCustomBlockDataBuilder) super.creativeCategory(creativeCategories);
}
@Override
public NonVanillaCustomBlockDataBuilder creativeGroup(@Nullable String creativeGroup) {
return (NonVanillaCustomBlockDataBuilder) super.creativeGroup(creativeGroup);
}
@Override
public NonVanillaCustomBlockDataBuilder components(@NonNull CustomBlockComponents components) {
return (NonVanillaCustomBlockDataBuilder) super.components(components);
}
@Override
public NonVanillaCustomBlockDataBuilder booleanProperty(@NonNull String propertyName) {
return (NonVanillaCustomBlockDataBuilder) super.booleanProperty(propertyName);
}
@Override
public NonVanillaCustomBlockDataBuilder intProperty(@NonNull String propertyName, List<Integer> values) {
return (NonVanillaCustomBlockDataBuilder) super.intProperty(propertyName, values);
}
@Override
public NonVanillaCustomBlockDataBuilder stringProperty(@NonNull String propertyName, List<String> values) {
return (NonVanillaCustomBlockDataBuilder) super.stringProperty(propertyName, values);
}
@Override
public NonVanillaCustomBlockDataBuilder permutations(@NonNull List<CustomBlockPermutation> permutations) {
return (NonVanillaCustomBlockDataBuilder) super.permutations(permutations);
}
@Override
public NonVanillaCustomBlockData build() {
return new GeyserNonVanillaCustomBlockData(this);
}
}
}

View File

@ -0,0 +1,306 @@
/*
* 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 it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.pack.ResourcePackManifest;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.type.CustomSkull;
import org.geysermc.geyser.skin.SkinProvider;
import org.geysermc.geyser.util.FileUtils;
import javax.imageio.ImageIO;
import java.awt.*;
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.List;
import java.util.*;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;
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 = 8;
private static final Path SKULL_SKIN_CACHE_PATH = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache").resolve("player_skulls");
public static final Map<String, Path> SKULL_SKINS = new Object2ObjectOpenHashMap<>();
@SuppressWarnings("ResultOfMethodCallIgnored")
public static Path createResourcePack() {
Path cachePath = GeyserImpl.getInstance().getBootstrap().getConfigFolder().resolve("cache");
try {
Files.createDirectories(cachePath);
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().severe("Unable to create directories for player skull resource pack!", e);
return null;
}
cleanSkullSkinCache();
Path packPath = cachePath.resolve("player_skulls.mcpack");
File packFile = packPath.toFile();
if (BlockRegistries.CUSTOM_SKULLS.get().isEmpty() || !GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) {
packFile.delete(); // No need to keep resource pack
return null;
}
if (packFile.exists() && canReusePack(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);
addSkinTextures(zipOS);
addAttachables(zipOS);
GeyserImpl.getInstance().getLogger().info("Finished creating skull resource pack.");
return packPath;
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().severe("Unable to create player skull resource pack!", e);
GeyserImpl.getInstance().getLogger().severe("Bedrock players will see dirt blocks instead of custom skull blocks.");
packFile.delete();
}
return null;
}
public static void cacheSkullSkin(String skinHash) throws IOException {
String skinUrl = Constants.MINECRAFT_SKIN_SERVER_URL + skinHash;
Path skinPath = SKULL_SKINS.get(skinHash);
if (skinPath != null) {
return;
}
Files.createDirectories(SKULL_SKIN_CACHE_PATH);
skinPath = SKULL_SKIN_CACHE_PATH.resolve(skinHash + ".png");
if (Files.exists(skinPath)) {
SKULL_SKINS.put(skinHash, skinPath);
return;
}
BufferedImage image = SkinProvider.requestImage(skinUrl, null);
// Resize skins to 48x16 to save on space and memory
BufferedImage skullTexture = new BufferedImage(48, 16, image.getType());
// Reorder skin parts to fit into the space
// Right, Front, Left, Back, Top, Bottom - head
// Right, Front, Left, Back, Top, Bottom - hat
Graphics g = skullTexture.createGraphics();
// Right, Front, Left, Back of the head
g.drawImage(image, 0, 0, 32, 8, 0, 8, 32, 16, null);
// Right, Front, Left, Back of the hat
g.drawImage(image, 0, 8, 32, 16, 32, 8, 64, 16, null);
// Top and bottom of the head
g.drawImage(image, 32, 0, 48, 8, 8, 0, 24, 8, null);
// Top and bottom of the hat
g.drawImage(image, 32, 8, 48, 16, 40, 0, 56, 8, null);
g.dispose();
image.flush();
ImageIO.write(skullTexture, "png", skinPath.toFile());
SKULL_SKINS.put(skinHash, skinPath);
GeyserImpl.getInstance().getLogger().debug("Cached player skull to " + skinPath + " for " + skinHash);
}
public static void cleanSkullSkinCache() {
try (Stream<Path> stream = Files.list(SKULL_SKIN_CACHE_PATH)) {
int removeCount = 0;
for (Path path : stream.toList()) {
String skinHash = path.getFileName().toString();
skinHash = skinHash.substring(0, skinHash.length() - ".png".length());
if (!SKULL_SKINS.containsKey(skinHash) && path.toFile().delete()) {
removeCount++;
}
}
if (removeCount != 0) {
GeyserImpl.getInstance().getLogger().debug("Removed " + removeCount + " unnecessary skull skins.");
}
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().debug("Unable to clean up skull skin cache.");
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
e.printStackTrace();
}
}
}
private static void addBaseResources(ZipOutputStream zipOS) 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, SkullResourcePackManager::fillManifestJson);
case "skull_resource_pack/textures/terrain_texture.json" ->
fillTemplate(zipOS, resourcePath, SkullResourcePackManager::fillTerrainTextureJson);
default -> zipOS.write(FileUtils.readAllBytes(resourcePath));
}
zipOS.closeEntry();
}
addFloorGeometries(zipOS);
ZipEntry entry = new ZipEntry("skull_resource_pack/pack_icon.png");
zipOS.putNextEntry(entry);
zipOS.write(FileUtils.readAllBytes("icon.png"));
zipOS.closeEntry();
}
}
private static void addFloorGeometries(ZipOutputStream zipOS) throws IOException {
String template = FileUtils.readToString("bedrock/skull_resource_pack/models/blocks/player_skull_floor.geo.json");
String[] quadrants = {"a", "b", "c", "d"};
for (int i = 0; i < quadrants.length; i++) {
String quadrant = quadrants[i];
float yRotation = i * 22.5f;
String contents = template
.replace("${quadrant}", quadrant)
.replace("${y_rotation}", String.valueOf(yRotation));
ZipEntry entry = new ZipEntry("skull_resource_pack/models/blocks/player_skull_floor_" + quadrant + ".geo.json");
zipOS.putNextEntry(entry);
zipOS.write(contents.getBytes(StandardCharsets.UTF_8));
zipOS.closeEntry();
}
}
private static void addAttachables(ZipOutputStream zipOS) throws IOException {
String template = FileUtils.readToString("bedrock/skull_resource_pack/attachables/template_attachable.json");
for (CustomSkull skull : BlockRegistries.CUSTOM_SKULLS.get().values()) {
ZipEntry entry = new ZipEntry("skull_resource_pack/attachables/" + truncateHash(skull.getSkinHash()) + ".json");
zipOS.putNextEntry(entry);
zipOS.write(fillAttachableJson(template, skull).getBytes(StandardCharsets.UTF_8));
zipOS.closeEntry();
}
}
private static void addSkinTextures(ZipOutputStream zipOS) throws IOException {
for (Path skinPath : SKULL_SKINS.values()) {
ZipEntry entry = new ZipEntry("skull_resource_pack/textures/blocks/" + truncateHash(skinPath.getFileName().toString()) + ".png");
zipOS.putNextEntry(entry);
try (InputStream stream = Files.newInputStream(skinPath)) {
stream.transferTo(zipOS);
}
zipOS.closeEntry();
}
}
private static void fillTemplate(ZipOutputStream zipOS, String path, UnaryOperator<String> filler) throws IOException {
String template = FileUtils.readToString(path);
String result = filler.apply(template);
zipOS.write(result.getBytes(StandardCharsets.UTF_8));
}
private static String fillAttachableJson(String template, CustomSkull skull) {
return template.replace("${identifier}", skull.getCustomBlockData().identifier())
.replace("${texture}", truncateHash(skull.getSkinHash()));
}
private static String fillManifestJson(String template) {
Pair<UUID, UUID> uuids = generatePackUUIDs();
return template.replace("${uuid1}", uuids.first().toString())
.replace("${uuid2}", uuids.second().toString());
}
private static String fillTerrainTextureJson(String template) {
StringBuilder textures = new StringBuilder();
for (String skinHash : SKULL_SKINS.keySet()) {
String texture = String.format("\"geyser.%s_player_skin\":{\"textures\":\"textures/blocks/%s\"},\n", skinHash, truncateHash(skinHash));
textures.append(texture);
}
if (textures.length() != 0) {
// Remove trailing comma
textures.delete(textures.length() - 2, textures.length());
}
return template.replace("${texture_data}", textures);
}
private static Pair<UUID, UUID> generatePackUUIDs() {
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));
}
SKULL_SKINS.keySet().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(File packFile) {
Pair<UUID, UUID> uuids = generatePackUUIDs();
try (ZipFile zipFile = new ZipFile(packFile)) {
Optional<? extends ZipEntry> manifestEntry = zipFile.stream()
.filter(entry -> entry.getName().contains("manifest.json"))
.findFirst();
if (manifestEntry.isPresent()) {
GeyserResourcePackManifest manifest = FileUtils.loadJson(zipFile.getInputStream(manifestEntry.get()), GeyserResourcePackManifest.class);
if (!uuids.first().equals(manifest.header().uuid())) {
return false;
}
Optional<UUID> resourceUUID = manifest.modules().stream()
.filter(module -> "resources".equals(module.type()))
.findFirst()
.map(ResourcePackManifest.Module::uuid);
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;
}
private static String truncateHash(String hash) {
return hash.substring(0, Math.min(hash.length(), 32));
}
}

View File

@ -25,17 +25,30 @@
package org.geysermc.geyser.registry;
import it.unimi.dsi.fastutil.Pair;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import org.geysermc.geyser.api.block.custom.CustomBlockData;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockItem;
import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState;
import org.geysermc.geyser.registry.loader.CollisionRegistryLoader;
import org.geysermc.geyser.registry.loader.RegistryLoaders;
import org.geysermc.geyser.registry.populator.BlockRegistryPopulator;
import org.geysermc.geyser.registry.populator.CustomBlockRegistryPopulator;
import org.geysermc.geyser.registry.populator.CustomSkullRegistryPopulator;
import org.geysermc.geyser.registry.type.BlockMapping;
import org.geysermc.geyser.registry.type.BlockMappings;
import org.geysermc.geyser.registry.type.CustomSkull;
import org.geysermc.geyser.translator.collision.BlockCollision;
import java.util.BitSet;
import java.util.Set;
/**
* Holds all the block registries in Geyser.
*/
@ -57,6 +70,11 @@ public class BlockRegistries {
*/
public static final ArrayRegistry<BlockMapping> JAVA_BLOCKS = ArrayRegistry.create(RegistryLoaders.uninitialized());
/**
* A mapped registry containing which holds block IDs to its {@link BlockCollision}.
*/
public static final IntMappedRegistry<BlockCollision> COLLISIONS;
/**
* A mapped registry containing the Java identifiers to IDs.
*/
@ -83,11 +101,51 @@ public class BlockRegistries {
*/
public static final SimpleRegistry<BitSet> INTERACTIVE_MAY_BUILD = SimpleRegistry.create(RegistryLoaders.uninitialized());
/**
* A registry containing all the custom blocks.
*/
public static final ArrayRegistry<CustomBlockData> CUSTOM_BLOCKS = ArrayRegistry.create(RegistryLoaders.empty(() -> new CustomBlockData[] {}));
/**
* A registry which stores Java Ids and the custom block state it should be replaced with.
*/
public static final MappedRegistry<Integer, CustomBlockState, Int2ObjectMap<CustomBlockState>> CUSTOM_BLOCK_STATE_OVERRIDES = MappedRegistry.create(RegistryLoaders.empty(Int2ObjectOpenHashMap::new));
/**
* A registry which stores non vanilla java blockstates and the custom block state it should be replaced with.
*/
public static final SimpleMappedRegistry<JavaBlockState, CustomBlockState> NON_VANILLA_BLOCK_STATE_OVERRIDES = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new));
/**
* A registry which stores clean Java Ids and the custom block it should be replaced with in the context of items.
*/
public static final SimpleMappedRegistry<String, CustomBlockData> CUSTOM_BLOCK_ITEM_OVERRIDES = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new));
/**
* A registry which stores Custom Block Data for extended collision boxes and the Java IDs of blocks that will have said extended collision boxes placed above them.
*/
public static final SimpleMappedRegistry<CustomBlockData, Set<Integer>> EXTENDED_COLLISION_BOXES = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new));
/**
* A registry which stores skin texture hashes to custom skull blocks.
*/
public static final SimpleMappedRegistry<String, CustomSkull> CUSTOM_SKULLS = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new));
static {
BlockRegistryPopulator.populate();
CustomSkullRegistryPopulator.populate();
BlockRegistryPopulator.populate(BlockRegistryPopulator.Stage.PRE_INIT);
CustomBlockRegistryPopulator.populate(CustomBlockRegistryPopulator.Stage.DEFINITION);
CustomBlockRegistryPopulator.populate(CustomBlockRegistryPopulator.Stage.NON_VANILLA_REGISTRATION);
BlockRegistryPopulator.populate(BlockRegistryPopulator.Stage.INIT_JAVA);
COLLISIONS = IntMappedRegistry.create(Pair.of("org.geysermc.geyser.translator.collision.CollisionRemapper", "mappings/collision.json"), CollisionRegistryLoader::new);
CustomBlockRegistryPopulator.populate(CustomBlockRegistryPopulator.Stage.VANILLA_REGISTRATION);
CustomBlockRegistryPopulator.populate(CustomBlockRegistryPopulator.Stage.CUSTOM_REGISTRATION);
BlockRegistryPopulator.populate(BlockRegistryPopulator.Stage.INIT_BEDROCK);
BlockRegistryPopulator.populate(BlockRegistryPopulator.Stage.POST_INIT);
}
public static void init() {
// no-op
}
}

View File

@ -31,7 +31,6 @@ import com.github.steveice10.mc.protocol.data.game.level.event.LevelEvent;
import com.github.steveice10.mc.protocol.data.game.level.particle.ParticleType;
import com.github.steveice10.mc.protocol.data.game.recipe.RecipeType;
import com.github.steveice10.packetlib.packet.Packet;
import it.unimi.dsi.fastutil.Pair;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
@ -56,7 +55,6 @@ import org.geysermc.geyser.registry.type.EnchantmentData;
import org.geysermc.geyser.registry.type.ItemMappings;
import org.geysermc.geyser.registry.type.ParticleMapping;
import org.geysermc.geyser.registry.type.SoundMapping;
import org.geysermc.geyser.translator.collision.BlockCollision;
import org.geysermc.geyser.translator.level.block.entity.BlockEntityTranslator;
import org.geysermc.geyser.translator.level.event.LevelEventTranslator;
import org.geysermc.geyser.translator.sound.SoundInteractionTranslator;
@ -68,6 +66,12 @@ import java.util.*;
* Holds all the common registries in Geyser.
*/
public final class Registries {
/**
* A registry holding all the providers.
* This has to be initialized first to allow extensions to access providers during other registry events.
*/
public static final SimpleMappedRegistry<Class<?>, ProviderSupplier> PROVIDERS = SimpleMappedRegistry.create(new IdentityHashMap<>(), ProviderRegistryLoader::new);
/**
* A registry holding a CompoundTag of the known entity identifiers.
*/
@ -93,11 +97,6 @@ public final class Registries {
*/
public static final SimpleMappedRegistry<BlockEntityType, BlockEntityTranslator> BLOCK_ENTITIES = SimpleMappedRegistry.create("org.geysermc.geyser.translator.level.block.entity.BlockEntity", BlockEntityRegistryLoader::new);
/**
* A mapped registry containing which holds block IDs to its {@link BlockCollision}.
*/
public static final IntMappedRegistry<BlockCollision> COLLISIONS = IntMappedRegistry.create(Pair.of("org.geysermc.geyser.translator.collision.CollisionRemapper", "mappings/collision.json"), CollisionRegistryLoader::new);
/**
* A versioned registry which holds a {@link RecipeType} to a corresponding list of {@link RecipeData}.
*/
@ -144,11 +143,6 @@ public final class Registries {
*/
public static final VersionedRegistry<Set<PotionMixData>> POTION_MIXES;
/**
* A registry holding all the
*/
public static final SimpleMappedRegistry<Class<?>, ProviderSupplier> PROVIDERS = SimpleMappedRegistry.create(new IdentityHashMap<>(), ProviderRegistryLoader::new);
/**
* A versioned registry holding all the recipes, with the net ID being the key, and {@link GeyserRecipe} as the value.
*/

View File

@ -79,6 +79,11 @@ public class CollisionRegistryLoader extends MultiResourceRegistryLoader<String,
Map<BlockCollision, BlockCollision> collisionInstances = new Object2ObjectOpenHashMap<>();
for (int i = 0; i < blockMappings.length; i++) {
BlockMapping blockMapping = blockMappings[i];
if (blockMapping == null) {
GeyserImpl.getInstance().getLogger().warning("Missing block mapping for Java block " + i);
continue;
}
BlockCollision newCollision = instantiateCollision(blockMapping, annotationMap, collisionList);
if (newCollision != null) {

View File

@ -25,6 +25,12 @@
package org.geysermc.geyser.registry.loader;
import org.geysermc.geyser.api.block.custom.CustomBlockData;
import org.geysermc.geyser.api.block.custom.NonVanillaCustomBlockData;
import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents;
import org.geysermc.geyser.api.block.custom.component.GeometryComponent;
import org.geysermc.geyser.api.block.custom.component.MaterialInstance;
import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState;
import org.geysermc.geyser.api.command.Command;
import org.geysermc.geyser.api.event.EventRegistrar;
import org.geysermc.geyser.api.extension.Extension;
@ -37,6 +43,12 @@ import org.geysermc.geyser.event.GeyserEventRegistrar;
import org.geysermc.geyser.item.GeyserCustomItemData;
import org.geysermc.geyser.item.GeyserCustomItemOptions;
import org.geysermc.geyser.item.GeyserNonVanillaCustomItemData;
import org.geysermc.geyser.level.block.GeyserCustomBlockComponents;
import org.geysermc.geyser.level.block.GeyserCustomBlockData;
import org.geysermc.geyser.level.block.GeyserGeometryComponent;
import org.geysermc.geyser.level.block.GeyserJavaBlockState;
import org.geysermc.geyser.level.block.GeyserMaterialInstance;
import org.geysermc.geyser.level.block.GeyserNonVanillaCustomBlockData;
import org.geysermc.geyser.pack.path.GeyserPathPackCodec;
import org.geysermc.geyser.registry.provider.ProviderSupplier;
@ -52,6 +64,14 @@ public class ProviderRegistryLoader implements RegistryLoader<Map<Class<?>, Prov
public Map<Class<?>, ProviderSupplier> load(Map<Class<?>, ProviderSupplier> providers) {
// misc
providers.put(Command.Builder.class, args -> new GeyserCommandManager.CommandBuilder<>((Extension) args[0]));
providers.put(CustomBlockComponents.Builder.class, args -> new GeyserCustomBlockComponents.CustomBlockComponentsBuilder());
providers.put(CustomBlockData.Builder.class, args -> new GeyserCustomBlockData.CustomBlockDataBuilder());
providers.put(JavaBlockState.Builder.class, args -> new GeyserJavaBlockState.JavaBlockStateBuilder());
providers.put(NonVanillaCustomBlockData.Builder.class, args -> new GeyserNonVanillaCustomBlockData.NonVanillaCustomBlockDataBuilder());
providers.put(MaterialInstance.Builder.class, args -> new GeyserMaterialInstance.MaterialInstanceBuilder());
providers.put(GeometryComponent.Builder.class, args -> new GeyserGeometryComponent.GeometryComponentBuilder());
providers.put(EventRegistrar.class, args -> new GeyserEventRegistrar(args[0]));
providers.put(PathPackCodec.class, args -> new GeyserPathPackCodec((Path) args[0]));

View File

@ -30,6 +30,7 @@ import org.geysermc.geyser.api.event.lifecycle.GeyserLoadResourcePacksEvent;
import org.geysermc.geyser.api.pack.ResourcePack;
import org.geysermc.geyser.pack.GeyserResourcePack;
import org.geysermc.geyser.pack.GeyserResourcePackManifest;
import org.geysermc.geyser.pack.SkullResourcePackManager;
import org.geysermc.geyser.pack.path.GeyserPathPackCodec;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.util.FileUtils;
@ -87,6 +88,12 @@ public class ResourcePackLoader implements RegistryLoader<Path, Map<String, Reso
resourcePacks = new ArrayList<>();
}
// Add custom skull pack
Path skullResourcePack = SkullResourcePackManager.createResourcePack();
if (skullResourcePack != null) {
resourcePacks.add(skullResourcePack);
}
GeyserLoadResourcePacksEvent event = new GeyserLoadResourcePacksEvent(resourcePacks);
GeyserImpl.getInstance().eventBus().fire(event);

View File

@ -23,15 +23,16 @@
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.item.mappings;
package org.geysermc.geyser.registry.mappings;
import com.fasterxml.jackson.databind.JsonNode;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.item.custom.CustomItemData;
import org.geysermc.geyser.item.mappings.versions.MappingsReader;
import org.geysermc.geyser.item.mappings.versions.MappingsReader_v1;
import org.geysermc.geyser.registry.mappings.util.CustomBlockMapping;
import org.geysermc.geyser.registry.mappings.versions.MappingsReader;
import org.geysermc.geyser.registry.mappings.versions.MappingsReader_v1;
import java.io.IOException;
import java.nio.file.Files;
@ -56,43 +57,88 @@ public class MappingsConfigReader {
}
}
public void loadMappingsFromJson(BiConsumer<String, CustomItemData> consumer) {
Path customMappingsDirectory = this.customMappingsDirectory;
if (!Files.exists(customMappingsDirectory)) {
public boolean ensureMappingsDirectory(Path mappingsDirectory) {
if (!Files.exists(mappingsDirectory)) {
try {
Files.createDirectories(customMappingsDirectory);
Files.createDirectories(mappingsDirectory);
return true;
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().error("Failed to create custom mappings directory", e);
return;
GeyserImpl.getInstance().getLogger().error("Failed to create mappings directory", e);
return false;
}
}
return true;
}
public void loadItemMappingsFromJson(BiConsumer<String, CustomItemData> consumer) {
if (!ensureMappingsDirectory(this.customMappingsDirectory)) {
return;
}
Path[] mappingsFiles = this.getCustomMappingsFiles();
for (Path mappingsFile : mappingsFiles) {
this.readMappingsFromJson(mappingsFile, consumer);
this.readItemMappingsFromJson(mappingsFile, consumer);
}
}
public void readMappingsFromJson(Path file, BiConsumer<String, CustomItemData> consumer) {
public void loadBlockMappingsFromJson(BiConsumer<String, CustomBlockMapping> consumer) {
if (!ensureMappingsDirectory(this.customMappingsDirectory)) {
return;
}
Path[] mappingsFiles = this.getCustomMappingsFiles();
for (Path mappingsFile : mappingsFiles) {
this.readBlockMappingsFromJson(mappingsFile, consumer);
}
}
public JsonNode getMappingsRoot(Path file) {
JsonNode mappingsRoot;
try {
mappingsRoot = GeyserImpl.JSON_MAPPER.readTree(file.toFile());
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().error("Failed to read custom mapping file: " + file, e);
return;
return null;
}
if (!mappingsRoot.has("format_version")) {
GeyserImpl.getInstance().getLogger().error("Mappings file " + file + " is missing the format version field!");
return;
return null;
}
int formatVersion = mappingsRoot.get("format_version").asInt();
return mappingsRoot;
}
public int getFormatVersion(JsonNode mappingsRoot, Path file) {
int formatVersion = mappingsRoot.get("format_version").asInt();
if (!this.mappingReaders.containsKey(formatVersion)) {
GeyserImpl.getInstance().getLogger().error("Mappings file " + file + " has an unknown format version: " + formatVersion);
return -1;
}
return formatVersion;
}
public void readItemMappingsFromJson(Path file, BiConsumer<String, CustomItemData> consumer) {
JsonNode mappingsRoot = getMappingsRoot(file);
int formatVersion = getFormatVersion(mappingsRoot, file);
if (formatVersion < 0 || mappingsRoot == null) {
return;
}
this.mappingReaders.get(formatVersion).readMappings(file, mappingsRoot, consumer);
this.mappingReaders.get(formatVersion).readItemMappings(file, mappingsRoot, consumer);
}
public void readBlockMappingsFromJson(Path file, BiConsumer<String, CustomBlockMapping> consumer) {
JsonNode mappingsRoot = getMappingsRoot(file);
int formatVersion = getFormatVersion(mappingsRoot, file);
if (formatVersion < 0 || mappingsRoot == null) {
return;
}
this.mappingReaders.get(formatVersion).readBlockMappings(file, mappingsRoot, consumer);
}
}

View File

@ -0,0 +1,40 @@
/*
* 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.registry.mappings.util;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.block.custom.component.BoxComponent;
import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents;
/**
* This class is used to store a custom block components mapping, which contains custom
* block components and a potenially null extended collision box
*
* @param components The components of the block
* @param extendedCollisionBox The extended collision box of the block
*/
public record CustomBlockComponentsMapping(@NonNull CustomBlockComponents components, BoxComponent extendedCollisionBox) {
}

View File

@ -0,0 +1,44 @@
/*
* 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.registry.mappings.util;
import java.util.Map;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.block.custom.CustomBlockData;
/**
* This class is used to store a custom block mappings, which contain all of the
* data required to register a custom block that overrides a group of java block
* states.
*
* @param data The custom block data
* @param states The custom block state mappings
* @param javaIdentifier The java identifier of the block
* @param overrideItem Whether or not the custom block should override the java item
*/
public record CustomBlockMapping(@NonNull CustomBlockData data, @NonNull Map<String, CustomBlockStateMapping> states, @NonNull String javaIdentifier, boolean overrideItem) {
}

View File

@ -0,0 +1,42 @@
/*
* 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.registry.mappings.util;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.api.block.custom.component.BoxComponent;
import java.util.function.Function;
/**
* This class is used to store a custom block state builder mapping, which contains custom
* block state builders and a potenially null extended collision box
*
* @param builder The builder of the block
* @param extendedCollisionBox The extended collision box of the block
*/
public record CustomBlockStateBuilderMapping(@NonNull Function<CustomBlockState.Builder, CustomBlockState> builder, BoxComponent extendedCollisionBox) {
}

View File

@ -0,0 +1,40 @@
/*
* 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.registry.mappings.util;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.api.block.custom.component.BoxComponent;
/**
* This class is used to store a custom block state mapping, which contains custom
* block states and a potenially null extended collision box
*
* @param state The state of the block
* @param extendedCollisionBox The extended collision box of the block
*/
public record CustomBlockStateMapping(@NonNull CustomBlockState state, BoxComponent extendedCollisionBox) {
}

View File

@ -23,20 +23,23 @@
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.item.mappings.versions;
package org.geysermc.geyser.registry.mappings.versions;
import com.fasterxml.jackson.databind.JsonNode;
import org.geysermc.geyser.api.item.custom.CustomItemData;
import org.geysermc.geyser.api.item.custom.CustomRenderOffsets;
import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException;
import org.geysermc.geyser.registry.mappings.util.CustomBlockMapping;
import java.nio.file.Path;
import java.util.function.BiConsumer;
public abstract class MappingsReader {
public abstract void readMappings(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomItemData> consumer);
public abstract void readItemMappings(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomItemData> consumer);
public abstract void readBlockMappings(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomBlockMapping> consumer);
public abstract CustomItemData readItemMappingEntry(JsonNode node) throws InvalidCustomMappingsFileException;
public abstract CustomBlockMapping readBlockMappingEntry(String identifier, JsonNode node) throws InvalidCustomMappingsFileException;
protected CustomRenderOffsets fromJsonNode(JsonNode node) {
if (node == null || !node.isObject()) {

View File

@ -0,0 +1,713 @@
/*
* 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.registry.mappings.versions;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.github.steveice10.mc.protocol.data.game.Identifier;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.block.custom.CustomBlockData;
import org.geysermc.geyser.api.block.custom.CustomBlockPermutation;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.api.block.custom.component.*;
import org.geysermc.geyser.api.block.custom.component.PlacementConditions.BlockFilterType;
import org.geysermc.geyser.api.block.custom.component.PlacementConditions.Face;
import org.geysermc.geyser.api.item.custom.CustomItemData;
import org.geysermc.geyser.api.item.custom.CustomItemOptions;
import org.geysermc.geyser.api.util.CreativeCategory;
import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException;
import org.geysermc.geyser.level.block.GeyserCustomBlockComponents.CustomBlockComponentsBuilder;
import org.geysermc.geyser.level.block.GeyserCustomBlockData.CustomBlockDataBuilder;
import org.geysermc.geyser.level.block.GeyserGeometryComponent.GeometryComponentBuilder;
import org.geysermc.geyser.level.block.GeyserMaterialInstance.MaterialInstanceBuilder;
import org.geysermc.geyser.level.physics.BoundingBox;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.mappings.util.CustomBlockComponentsMapping;
import org.geysermc.geyser.registry.mappings.util.CustomBlockMapping;
import org.geysermc.geyser.registry.mappings.util.CustomBlockStateBuilderMapping;
import org.geysermc.geyser.registry.mappings.util.CustomBlockStateMapping;
import org.geysermc.geyser.translator.collision.BlockCollision;
import org.geysermc.geyser.util.BlockUtils;
import org.geysermc.geyser.util.MathUtils;
import java.nio.file.Path;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
/**
* A class responsible for reading custom item and block mappings from a JSON file
*/
public class MappingsReader_v1 extends MappingsReader {
@Override
public void readItemMappings(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomItemData> consumer) {
this.readItemMappingsV1(file, mappingsRoot, consumer);
}
/**
* Read item block from a JSON node
*
* @param file The path to the file
* @param mappingsRoot The {@link JsonNode} containing the mappings
* @param consumer The consumer to accept the mappings
* @see #readBlockMappingsV1(Path, JsonNode, BiConsumer)
*/
@Override
public void readBlockMappings(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomBlockMapping> consumer) {
this.readBlockMappingsV1(file, mappingsRoot, consumer);
}
public void readItemMappingsV1(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomItemData> consumer) {
JsonNode itemsNode = mappingsRoot.get("items");
if (itemsNode != null && itemsNode.isObject()) {
itemsNode.fields().forEachRemaining(entry -> {
if (entry.getValue().isArray()) {
entry.getValue().forEach(data -> {
try {
CustomItemData customItemData = this.readItemMappingEntry(data);
consumer.accept(entry.getKey(), customItemData);
} catch (InvalidCustomMappingsFileException e) {
GeyserImpl.getInstance().getLogger().error("Error in registering items for custom mapping file: " + file.toString(), e);
}
});
}
});
}
}
/**
* Read block mappings from a JSON node
*
* @param file The path to the file
* @param mappingsRoot The {@link JsonNode} containing the mappings
* @param consumer The consumer to accept the mappings
* @see #readBlockMappings(Path, JsonNode, BiConsumer)
*/
public void readBlockMappingsV1(Path file, JsonNode mappingsRoot, BiConsumer<String, CustomBlockMapping> consumer) {
JsonNode blocksNode = mappingsRoot.get("blocks");
if (blocksNode != null && blocksNode.isObject()) {
blocksNode.fields().forEachRemaining(entry -> {
if (entry.getValue().isObject()) {
try {
String identifier = Identifier.formalize(entry.getKey());
CustomBlockMapping customBlockMapping = this.readBlockMappingEntry(identifier, entry.getValue());
consumer.accept(identifier, customBlockMapping);
} catch (Exception e) {
GeyserImpl.getInstance().getLogger().error("Error in registering blocks for custom mapping file: " + file.toString());
GeyserImpl.getInstance().getLogger().error("due to entry: " + entry, e);
}
}
});
}
}
private CustomItemOptions readItemCustomItemOptions(JsonNode node) {
CustomItemOptions.Builder customItemOptions = CustomItemOptions.builder();
JsonNode customModelData = node.get("custom_model_data");
if (customModelData != null && customModelData.isInt()) {
customItemOptions.customModelData(customModelData.asInt());
}
JsonNode damagePredicate = node.get("damage_predicate");
if (damagePredicate != null && damagePredicate.isInt()) {
customItemOptions.damagePredicate(damagePredicate.asInt());
}
JsonNode unbreakable = node.get("unbreakable");
if (unbreakable != null && unbreakable.isBoolean()) {
customItemOptions.unbreakable(unbreakable.asBoolean());
}
JsonNode defaultItem = node.get("default");
if (defaultItem != null && defaultItem.isBoolean()) {
customItemOptions.defaultItem(defaultItem.asBoolean());
}
return customItemOptions.build();
}
@Override
public CustomItemData readItemMappingEntry(JsonNode node) throws InvalidCustomMappingsFileException {
if (node == null || !node.isObject()) {
throw new InvalidCustomMappingsFileException("Invalid item mappings entry");
}
JsonNode name = node.get("name");
if (name == null || !name.isTextual() || name.asText().isEmpty()) {
throw new InvalidCustomMappingsFileException("An item entry has no name");
}
CustomItemData.Builder customItemData = CustomItemData.builder()
.name(name.asText())
.customItemOptions(this.readItemCustomItemOptions(node));
//The next entries are optional
if (node.has("display_name")) {
customItemData.displayName(node.get("display_name").asText());
}
if (node.has("icon")) {
customItemData.icon(node.get("icon").asText());
}
if (node.has("allow_offhand")) {
customItemData.allowOffhand(node.get("allow_offhand").asBoolean());
}
if (node.has("display_handheld")) {
customItemData.displayHandheld(node.get("display_handheld").asBoolean());
}
if (node.has("texture_size")) {
customItemData.textureSize(node.get("texture_size").asInt());
}
if (node.has("render_offsets")) {
JsonNode tmpNode = node.get("render_offsets");
customItemData.renderOffsets(fromJsonNode(tmpNode));
}
return customItemData.build();
}
/**
* Read a block mapping entry from a JSON node and Java identifier
*
* @param identifier The Java identifier of the block
* @param node The {@link JsonNode} containing the block mapping entry
* @return The {@link CustomBlockMapping} record to be read by {@link org.geysermc.geyser.registry.populator.CustomBlockRegistryPopulator}
* @throws InvalidCustomMappingsFileException If the JSON node is invalid
*/
@Override
public CustomBlockMapping readBlockMappingEntry(String identifier, JsonNode node) throws InvalidCustomMappingsFileException {
if (node == null || !node.isObject()) {
throw new InvalidCustomMappingsFileException("Invalid block mappings entry:" + node);
}
String name = node.get("name").asText();
if (name == null || name.isEmpty()) {
throw new InvalidCustomMappingsFileException("A block entry has no name");
}
boolean includedInCreativeInventory = node.has("included_in_creative_inventory") && node.get("included_in_creative_inventory").asBoolean();
CreativeCategory creativeCategory = CreativeCategory.NONE;
if (node.has("creative_category")) {
String categoryName = node.get("creative_category").asText();
try {
creativeCategory = CreativeCategory.valueOf(categoryName.toUpperCase());
} catch (IllegalArgumentException e) {
throw new InvalidCustomMappingsFileException("Invalid creative category \"" + categoryName + "\" for block \"" + name + "\"");
}
}
String creativeGroup = "";
if (node.has("creative_group")) {
creativeGroup = node.get("creative_group").asText();
}
// If this is true, we will only register the states the user has specified rather than all the possible block states
boolean onlyOverrideStates = node.has("only_override_states") && node.get("only_override_states").asBoolean();
// Create the data for the overall block
CustomBlockData.Builder customBlockDataBuilder = new CustomBlockDataBuilder()
.name(name)
.includedInCreativeInventory(includedInCreativeInventory)
.creativeCategory(creativeCategory)
.creativeGroup(creativeGroup);
if (BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().containsKey(identifier)) {
// There is only one Java block state to override
CustomBlockComponentsMapping componentsMapping = createCustomBlockComponentsMapping(node, identifier, name);
CustomBlockData blockData = customBlockDataBuilder
.components(componentsMapping.components())
.build();
return new CustomBlockMapping(blockData, Map.of(identifier, new CustomBlockStateMapping(blockData.defaultBlockState(), componentsMapping.extendedCollisionBox())), identifier, !onlyOverrideStates);
}
Map<String, CustomBlockComponentsMapping> componentsMap = new LinkedHashMap<>();
JsonNode stateOverrides = node.get("state_overrides");
if (stateOverrides != null && stateOverrides.isObject()) {
// Load components for specific Java block states
Iterator<Map.Entry<String, JsonNode>> fields = stateOverrides.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> overrideEntry = fields.next();
String state = identifier + "[" + overrideEntry.getKey() + "]";
if (!BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().containsKey(state)) {
throw new InvalidCustomMappingsFileException("Unknown Java block state: " + state + " for state_overrides.");
}
componentsMap.put(state, createCustomBlockComponentsMapping(overrideEntry.getValue(), state, name));
}
}
if (componentsMap.isEmpty() && onlyOverrideStates) {
throw new InvalidCustomMappingsFileException("Block entry for " + identifier + " has only_override_states set to true, but has no state_overrides.");
}
if (!onlyOverrideStates) {
// Create components for any remaining Java block states
BlockRegistries.JAVA_IDENTIFIER_TO_ID.get().keySet()
.stream()
.filter(s -> s.startsWith(identifier + "["))
.filter(Predicate.not(componentsMap::containsKey))
.forEach(state -> componentsMap.put(state, createCustomBlockComponentsMapping(null, state, name)));
}
if (componentsMap.isEmpty()) {
throw new InvalidCustomMappingsFileException("Unknown Java block: " + identifier);
}
// We pass in the first state and just use the hitbox from that as the default
// Each state will have its own so this is fine
String firstState = componentsMap.keySet().iterator().next();
CustomBlockComponentsMapping firstComponentsMapping = createCustomBlockComponentsMapping(node, firstState, name);
customBlockDataBuilder.components(firstComponentsMapping.components());
return createCustomBlockMapping(customBlockDataBuilder, componentsMap, identifier, !onlyOverrideStates);
}
private CustomBlockMapping createCustomBlockMapping(CustomBlockData.Builder customBlockDataBuilder, Map<String, CustomBlockComponentsMapping> componentsMap, String identifier, boolean overrideItem) {
Map<String, LinkedHashSet<String>> valuesMap = new Object2ObjectOpenHashMap<>();
List<CustomBlockPermutation> permutations = new ArrayList<>();
Map<String, CustomBlockStateBuilderMapping> blockStateBuilders = new Object2ObjectOpenHashMap<>();
// For each Java block state, extract the property values, create a CustomBlockPermutation,
// and a CustomBlockState builder
for (Map.Entry<String, CustomBlockComponentsMapping> entry : componentsMap.entrySet()) {
String state = entry.getKey();
String[] pairs = splitStateString(state);
String[] conditions = new String[pairs.length];
Function<CustomBlockState.Builder, CustomBlockState.Builder> blockStateBuilder = Function.identity();
for (int i = 0; i < pairs.length; i++) {
String[] parts = pairs[i].split("=");
String property = parts[0];
String value = parts[1];
valuesMap.computeIfAbsent(property, k -> new LinkedHashSet<>())
.add(value);
conditions[i] = String.format("q.block_property('%s') == '%s'", property, value);
blockStateBuilder = blockStateBuilder.andThen(builder -> builder.stringProperty(property, value));
}
permutations.add(new CustomBlockPermutation(entry.getValue().components(), String.join(" && ", conditions)));
blockStateBuilders.put(state, new CustomBlockStateBuilderMapping(blockStateBuilder.andThen(CustomBlockState.Builder::build), entry.getValue().extendedCollisionBox()));
}
valuesMap.forEach((key, value) -> customBlockDataBuilder.stringProperty(key, new ArrayList<>(value)));
CustomBlockData customBlockData = customBlockDataBuilder
.permutations(permutations)
.build();
// Build CustomBlockStates for each Java block state we wish to override
Map<String, CustomBlockStateMapping> states = blockStateBuilders.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> new CustomBlockStateMapping(e.getValue().builder().apply(customBlockData.blockStateBuilder()), e.getValue().extendedCollisionBox())));
return new CustomBlockMapping(customBlockData, states, identifier, overrideItem);
}
/**
* Creates a {@link CustomBlockComponents} object for the passed state override or base block node, Java block state identifier, and custom block name
*
* @param node the state override or base block {@link JsonNode}
* @param stateKey the Java block state identifier
* @param name the name of the custom block
* @return the {@link CustomBlockComponents} object
*/
private CustomBlockComponentsMapping createCustomBlockComponentsMapping(JsonNode node, String stateKey, String name) {
// This is needed to find the correct selection box for the given block
int id = BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(stateKey, -1);
BoxComponent boxComponent = createBoxComponent(id);
BoxComponent extendedBoxComponent = createExtendedBoxComponent(id);
CustomBlockComponents.Builder builder = new CustomBlockComponentsBuilder()
.collisionBox(boxComponent)
.selectionBox(boxComponent);
if (node == null) {
// No other components were defined
return new CustomBlockComponentsMapping(builder.build(), extendedBoxComponent);
}
BoxComponent selectionBox = createBoxComponent(node.get("selection_box"));
if (selectionBox != null) {
builder.selectionBox(selectionBox);
}
BoxComponent collisionBox = createBoxComponent(node.get("collision_box"));
if (collisionBox != null) {
builder.collisionBox(collisionBox);
}
BoxComponent extendedCollisionBox = createBoxComponent(node.get("extended_collision_box"));
if (extendedCollisionBox != null) {
extendedBoxComponent = extendedCollisionBox;
}
// We set this to max value by default so that we may dictate the correct destroy time ourselves
float destructibleByMining = Float.MAX_VALUE;
if (node.has("destructible_by_mining")) {
destructibleByMining = node.get("destructible_by_mining").floatValue();
}
builder.destructibleByMining(destructibleByMining);
if (node.has("geometry")) {
if (node.get("geometry").isTextual()) {
builder.geometry(new GeometryComponentBuilder()
.identifier(node.get("geometry").asText())
.build());
} else {
JsonNode geometry = node.get("geometry");
GeometryComponentBuilder geometryBuilder = new GeometryComponentBuilder();
if (geometry.has("identifier")) {
geometryBuilder.identifier(geometry.get("identifier").asText());
}
if (geometry.has("bone_visibility")) {
JsonNode boneVisibility = geometry.get("bone_visibility");
if (boneVisibility.isObject()) {
Map<String, String> boneVisibilityMap = new Object2ObjectOpenHashMap<>();
boneVisibility.fields().forEachRemaining(entry -> {
boneVisibilityMap.put(entry.getKey(), entry.getValue().isBoolean() ? (entry.getValue().asBoolean() ? "1" : "0") : entry.getValue().asText());
});
geometryBuilder.boneVisibility(boneVisibilityMap);
}
}
builder.geometry(geometryBuilder.build());
}
}
String displayName = name;
if (node.has("display_name")) {
displayName = node.get("display_name").asText();
}
builder.displayName(displayName);
if (node.has("friction")) {
builder.friction(node.get("friction").floatValue());
}
if (node.has("light_emission")) {
builder.lightEmission(node.get("light_emission").asInt());
}
if (node.has("light_dampening")) {
builder.lightDampening(node.get("light_dampening").asInt());
}
boolean placeAir = true;
if (node.has("place_air")) {
placeAir = node.get("place_air").asBoolean();
}
builder.placeAir(placeAir);
if (node.has("transformation")) {
JsonNode transformation = node.get("transformation");
int rotationX = 0;
int rotationY = 0;
int rotationZ = 0;
float scaleX = 1;
float scaleY = 1;
float scaleZ = 1;
float transformX = 0;
float transformY = 0;
float transformZ = 0;
if (transformation.has("rotation")) {
JsonNode rotation = transformation.get("rotation");
rotationX = rotation.get(0).asInt();
rotationY = rotation.get(1).asInt();
rotationZ = rotation.get(2).asInt();
}
if (transformation.has("scale")) {
JsonNode scale = transformation.get("scale");
scaleX = scale.get(0).floatValue();
scaleY = scale.get(1).floatValue();
scaleZ = scale.get(2).floatValue();
}
if (transformation.has("translation")) {
JsonNode translation = transformation.get("translation");
transformX = translation.get(0).floatValue();
transformY = translation.get(1).floatValue();
transformZ = translation.get(2).floatValue();
}
builder.transformation(new TransformationComponent(rotationX, rotationY, rotationZ, scaleX, scaleY, scaleZ, transformX, transformY, transformZ));
}
if (node.has("unit_cube")) {
builder.unitCube(node.get("unit_cube").asBoolean());
}
if (node.has("material_instances")) {
JsonNode materialInstances = node.get("material_instances");
if (materialInstances.isObject()) {
materialInstances.fields().forEachRemaining(entry -> {
String key = entry.getKey();
JsonNode value = entry.getValue();
if (value.isObject()) {
MaterialInstance materialInstance = createMaterialInstanceComponent(value, name);
builder.materialInstance(key, materialInstance);
}
});
}
}
if (node.has("placement_filter")) {
JsonNode placementFilter = node.get("placement_filter");
if (placementFilter.isObject()) {
if (placementFilter.has("conditions")) {
JsonNode conditions = placementFilter.get("conditions");
if (conditions.isArray()) {
List<PlacementConditions> filter = createPlacementFilterComponent(conditions);
builder.placementFilter(filter);
}
}
}
}
// Tags can be applied so that blocks will match return true when queried for the tag
// Potentially useful for resource pack creators
// Ideally we could programmatically extract the tags here https://wiki.bedrock.dev/blocks/block-tags.html
// This would let us automatically apply the correct vanilla tags to blocks
// However, its worth noting that vanilla tools do not currently honor these tags anyway
if (node.get("tags") instanceof ArrayNode tags) {
Set<String> tagsSet = new ObjectOpenHashSet<>();
tags.forEach(tag -> tagsSet.add(tag.asText()));
builder.tags(tagsSet);
}
return new CustomBlockComponentsMapping(builder.build(), extendedBoxComponent);
}
/**
* Creates a {@link BoxComponent} based on a Java block's collision with provided bounds and offsets
*
* @param javaId the block's Java ID
* @param heightTranslation the height translation of the box
* @return the {@link BoxComponent}
*/
private BoxComponent createBoxComponent(int javaId, float heightTranslation) {
// Some blocks (e.g. plants) have no collision box
BlockCollision blockCollision = BlockUtils.getCollision(javaId);
if (blockCollision == null || blockCollision.getBoundingBoxes().length == 0) {
return BoxComponent.emptyBox();
}
float minX = 5;
float minY = 5;
float minZ = 5;
float maxX = -5;
float maxY = -5;
float maxZ = -5;
for (BoundingBox boundingBox : blockCollision.getBoundingBoxes()) {
double offsetX = boundingBox.getSizeX() * 0.5;
double offsetY = boundingBox.getSizeY() * 0.5;
double offsetZ = boundingBox.getSizeZ() * 0.5;
minX = Math.min(minX, (float) (boundingBox.getMiddleX() - offsetX));
minY = Math.min(minY, (float) (boundingBox.getMiddleY() - offsetY));
minZ = Math.min(minZ, (float) (boundingBox.getMiddleZ() - offsetZ));
maxX = Math.max(maxX, (float) (boundingBox.getMiddleX() + offsetX));
maxY = Math.max(maxY, (float) (boundingBox.getMiddleY() + offsetY));
maxZ = Math.max(maxZ, (float) (boundingBox.getMiddleZ() + offsetZ));
}
minX = MathUtils.clamp(minX, 0, 1);
minY = MathUtils.clamp(minY + heightTranslation, 0, 1);
minZ = MathUtils.clamp(minZ, 0, 1);
maxX = MathUtils.clamp(maxX, 0, 1);
maxY = MathUtils.clamp(maxY + heightTranslation, 0, 1);
maxZ = MathUtils.clamp(maxZ, 0, 1);
return new BoxComponent(
16 * (1 - maxX) - 8, // For some odd reason X is mirrored on Bedrock
16 * minY,
16 * minZ - 8,
16 * (maxX - minX),
16 * (maxY - minY),
16 * (maxZ - minZ)
);
}
/**
* Creates a {@link BoxComponent} based on a Java block's collision
*
* @param javaId the block's Java ID
* @return the {@link BoxComponent}
*/
private BoxComponent createBoxComponent(int javaId) {
return createBoxComponent(javaId, 0);
}
/**
* Creates the {@link BoxComponent} for an extended collision box based on a Java block's collision
*
* @param javaId the block's Java ID
* @return the {@link BoxComponent} or null if the block's collision box would not exceed 16 y units
*/
private BoxComponent createExtendedBoxComponent(int javaId) {
BlockCollision blockCollision = BlockUtils.getCollision(javaId);
if (blockCollision == null) {
return null;
}
for (BoundingBox box : blockCollision.getBoundingBoxes()) {
double maxY = 0.5 * box.getSizeY() + box.getMiddleY();
if (maxY > 1) {
return createBoxComponent(javaId, -1);
}
}
return null;
}
/**
* Creates a {@link BoxComponent} from a JSON Node
*
* @param node the JSON node
* @return the {@link BoxComponent}
*/
private BoxComponent createBoxComponent(JsonNode node) {
if (node != null && node.isObject()) {
if (node.has("origin") && node.has("size")) {
JsonNode origin = node.get("origin");
float originX = origin.get(0).floatValue();
float originY = origin.get(1).floatValue();
float originZ = origin.get(2).floatValue();
JsonNode size = node.get("size");
float sizeX = size.get(0).floatValue();
float sizeY = size.get(1).floatValue();
float sizeZ = size.get(2).floatValue();
return new BoxComponent(originX, originY, originZ, sizeX, sizeY, sizeZ);
}
}
return null;
}
/**
* Creates the {@link MaterialInstance} for the passed material instance node and custom block name
* The name is used as a fallback if no texture is provided by the node
*
* @param node the material instance node
* @param name the custom block name
* @return the {@link MaterialInstance}
*/
private MaterialInstance createMaterialInstanceComponent(JsonNode node, String name) {
// Set default values, and use what the user provides if they have provided something
String texture = name;
if (node.has("texture")) {
texture = node.get("texture").asText();
}
String renderMethod = "opaque";
if (node.has("render_method")) {
renderMethod = node.get("render_method").asText();
}
boolean faceDimming = true;
if (node.has("face_dimming")) {
faceDimming = node.get("face_dimming").asBoolean();
}
boolean ambientOcclusion = true;
if (node.has("ambient_occlusion")) {
ambientOcclusion = node.get("ambient_occlusion").asBoolean();
}
return new MaterialInstanceBuilder()
.texture(texture)
.renderMethod(renderMethod)
.faceDimming(faceDimming)
.ambientOcclusion(ambientOcclusion)
.build();
}
/**
* Creates the list of {@link PlacementConditions} for the passed conditions node
*
* @param node the conditions node
* @return the list of {@link PlacementConditions}
*/
private List<PlacementConditions> createPlacementFilterComponent(JsonNode node) {
List<PlacementConditions> conditions = new ArrayList<>();
// The structure of the placement filter component is the most complex of the current components
// Each condition effectively separated into two arrays: one of allowed faces, and one of blocks/block Molang queries
node.forEach(condition -> {
Set<Face> faces = EnumSet.noneOf(Face.class);
if (condition.has("allowed_faces")) {
JsonNode allowedFaces = condition.get("allowed_faces");
if (allowedFaces.isArray()) {
allowedFaces.forEach(face -> faces.add(Face.valueOf(face.asText().toUpperCase())));
}
}
LinkedHashMap<String, BlockFilterType> blockFilters = new LinkedHashMap<>();
if (condition.has("block_filter")) {
JsonNode blockFilter = condition.get("block_filter");
if (blockFilter.isArray()) {
blockFilter.forEach(filter -> {
if (filter.isObject()) {
if (filter.has("tags")) {
JsonNode tags = filter.get("tags");
blockFilters.put(tags.asText(), BlockFilterType.TAG);
}
} else if (filter.isTextual()) {
blockFilters.put(filter.asText(), BlockFilterType.BLOCK);
}
});
}
}
conditions.add(new PlacementConditions(faces, blockFilters));
});
return conditions;
}
/**
* Splits the given java state identifier into an array of property=value pairs
*
* @param state the java state identifier
* @return the array of property=value pairs
*/
private String[] splitStateString(String state) {
int openBracketIndex = state.indexOf("[");
String states = state.substring(openBracketIndex + 1, state.length() - 1);
return states.split(",");
}
}

View File

@ -30,13 +30,19 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Interner;
import com.google.common.collect.Interners;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.*;
import org.cloudburstmc.nbt.*;
import org.cloudburstmc.protocol.bedrock.codec.v582.Bedrock_v582;
import org.cloudburstmc.protocol.bedrock.data.BlockPropertyData;
import org.cloudburstmc.protocol.bedrock.codec.v589.Bedrock_v589;
import org.cloudburstmc.protocol.bedrock.codec.v594.Bedrock_v594;
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.block.custom.CustomBlockData;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.level.physics.PistonBehavior;
import org.geysermc.geyser.registry.BlockRegistries;
@ -47,6 +53,7 @@ import org.geysermc.geyser.util.BlockUtils;
import java.io.DataInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.BiFunction;
import java.util.zip.GZIPInputStream;
@ -55,15 +62,35 @@ import java.util.zip.GZIPInputStream;
* Populates the block registries.
*/
public final class BlockRegistryPopulator {
/**
* The stage of population
*/
public enum Stage {
PRE_INIT,
INIT_JAVA,
INIT_BEDROCK,
POST_INIT;
}
public static void populate(Stage stage) {
switch (stage) {
case PRE_INIT -> { nullifyBlocksNode(); }
case INIT_JAVA -> { registerJavaBlocks(); }
case INIT_BEDROCK -> { registerBedrockBlocks(); }
case POST_INIT -> { nullifyBlocksNode(); }
default -> { throw new IllegalArgumentException("Unknown stage: " + stage); }
}
}
/**
* Stores the raw blocks JSON until it is no longer needed.
*/
private static JsonNode BLOCKS_JSON;
private static int minCustomRuntimeID = -1;
private static int maxCustomRuntimeID = -1;
private static int javaBlocksSize = -1;
public static void populate() {
registerJavaBlocks();
registerBedrockBlocks();
private static void nullifyBlocksNode() {
BLOCKS_JSON = null;
}
@ -154,36 +181,83 @@ public final class BlockRegistryPopulator {
Interner<NbtMap> statesInterner = Interners.newStrongInterner();
for (Map.Entry<ObjectIntPair<String>, BiFunction<String, NbtMapBuilder, String>> palette : blockMappers.entrySet()) {
NbtList<NbtMap> blocksTag;
int protocolVersion = palette.getKey().valueInt();
List<NbtMap> vanillaBlockStates;
List<NbtMap> blockStates;
try (InputStream stream = GeyserImpl.getInstance().getBootstrap().getResource(String.format("bedrock/block_palette.%s.nbt", palette.getKey().key()));
NBTInputStream nbtInputStream = new NBTInputStream(new DataInputStream(new GZIPInputStream(stream)), true, true)) {
NBTInputStream nbtInputStream = new NBTInputStream(new DataInputStream(new GZIPInputStream(stream)), true, true)) {
NbtMap blockPalette = (NbtMap) nbtInputStream.readTag();
blocksTag = (NbtList<NbtMap>) blockPalette.getList("blocks", NbtType.COMPOUND);
vanillaBlockStates = new ArrayList<>(blockPalette.getList("blocks", NbtType.COMPOUND));
for (int i = 0; i < vanillaBlockStates.size(); i++) {
NbtMapBuilder builder = vanillaBlockStates.get(i).toBuilder();
builder.remove("name_hash"); // Quick workaround - was added in 1.19.20
builder.remove("network_id"); // Added in 1.19.80 - ????
builder.putCompound("states", statesInterner.intern((NbtMap) builder.remove("states")));
vanillaBlockStates.set(i, builder.build());
}
blockStates = new ArrayList<>(vanillaBlockStates);
} catch (Exception e) {
throw new AssertionError("Unable to get blocks from runtime block states", e);
}
int stateVersion = vanillaBlockStates.get(0).getInt("version");
List<BlockPropertyData> customBlockProperties = new ArrayList<>();
List<NbtMap> customBlockStates = new ArrayList<>();
List<CustomBlockState> customExtBlockStates = new ArrayList<>();
int[] remappedVanillaIds = new int[0];
if (BlockRegistries.CUSTOM_BLOCKS.get().length != 0) {
for (CustomBlockData customBlock : BlockRegistries.CUSTOM_BLOCKS.get()) {
customBlockProperties.add(CustomBlockRegistryPopulator.generateBlockPropertyData(customBlock, protocolVersion));
CustomBlockRegistryPopulator.generateCustomBlockStates(customBlock, customBlockStates, customExtBlockStates, stateVersion);
}
blockStates.addAll(customBlockStates);
GeyserImpl.getInstance().getLogger().debug("Added " + customBlockStates.size() + " custom block states to v" + protocolVersion + " palette.");
// The palette is sorted by the FNV1 64-bit hash of the name
blockStates.sort((a, b) -> Long.compareUnsigned(fnv164(a.getString("name")), fnv164(b.getString("name"))));
}
// New since 1.16.100 - find the block runtime ID by the order given to us in the block palette,
// as we no longer send a block palette
Object2ObjectMap<NbtMap, GeyserBedrockBlock> blockStateOrderedMap = new Object2ObjectOpenHashMap<>(blocksTag.size());
GeyserBedrockBlock[] bedrockRuntimeMap = new GeyserBedrockBlock[blocksTag.size()];
int stateVersion = -1;
for (int i = 0; i < blocksTag.size(); i++) {
NbtMapBuilder builder = blocksTag.get(i).toBuilder();
builder.remove("name_hash"); // Quick workaround - was added in 1.19.20
builder.remove("network_id"); // Added in 1.19.80 - ????
builder.putCompound("states", statesInterner.intern((NbtMap) builder.remove("states")));
NbtMap tag = builder.build();
Object2ObjectMap<NbtMap, GeyserBedrockBlock> blockStateOrderedMap = new Object2ObjectOpenHashMap<>(blockStates.size());
GeyserBedrockBlock[] bedrockRuntimeMap = new GeyserBedrockBlock[blockStates.size()];
for (int i = 0; i < blockStates.size(); i++) {
NbtMap tag = blockStates.get(i);
if (blockStateOrderedMap.containsKey(tag)) {
throw new AssertionError("Duplicate block states in Bedrock palette: " + tag);
}
GeyserBedrockBlock block = new GeyserBedrockBlock(i, tag);
blockStateOrderedMap.put(tag, block);
bedrockRuntimeMap[i] = block;
if (stateVersion == -1) {
stateVersion = tag.getInt("version");
}
Object2ObjectMap<CustomBlockState, GeyserBedrockBlock> customBlockStateDefinitions = Object2ObjectMaps.emptyMap();
Int2ObjectMap<GeyserBedrockBlock> extendedCollisionBoxes = new Int2ObjectOpenHashMap<>();
if (BlockRegistries.CUSTOM_BLOCKS.get().length != 0) {
customBlockStateDefinitions = new Object2ObjectOpenHashMap<>(customExtBlockStates.size());
for (int i = 0; i < customExtBlockStates.size(); i++) {
NbtMap tag = customBlockStates.get(i);
CustomBlockState blockState = customExtBlockStates.get(i);
GeyserBedrockBlock bedrockBlock = blockStateOrderedMap.get(tag);
customBlockStateDefinitions.put(blockState, bedrockBlock);
Set<Integer> extendedCollisionjavaIds = BlockRegistries.EXTENDED_COLLISION_BOXES.getOrDefault(blockState.block(), null);
if (extendedCollisionjavaIds != null) {
for (int javaId : extendedCollisionjavaIds) {
extendedCollisionBoxes.put(javaId, bedrockBlock);
}
}
}
remappedVanillaIds = new int[vanillaBlockStates.size()];
for (int i = 0; i < vanillaBlockStates.size(); i++) {
GeyserBedrockBlock bedrockBlock = blockStateOrderedMap.get(vanillaBlockStates.get(i));
remappedVanillaIds[i] = bedrockBlock != null ? bedrockBlock.getRuntimeId() : -1;
}
}
int javaRuntimeId = -1;
GeyserBedrockBlock airDefinition = null;
@ -194,7 +268,8 @@ public final class BlockRegistryPopulator {
BiFunction<String, NbtMapBuilder, String> stateMapper = blockMappers.getOrDefault(palette.getKey(), emptyMapper);
GeyserBedrockBlock[] javaToBedrockBlocks = new GeyserBedrockBlock[BLOCKS_JSON.size()];
GeyserBedrockBlock[] javaToBedrockBlocks = new GeyserBedrockBlock[javaBlocksSize];
GeyserBedrockBlock[] javaToVanillaBedrockBlocks = new GeyserBedrockBlock[javaBlocksSize];
Map<String, NbtMap> flowerPotBlocks = new Object2ObjectOpenHashMap<>();
Map<NbtMap, BlockDefinition> itemFrames = new Object2ObjectOpenHashMap<>();
@ -206,11 +281,22 @@ public final class BlockRegistryPopulator {
javaRuntimeId++;
Map.Entry<String, JsonNode> entry = blocksIterator.next();
String javaId = entry.getKey();
GeyserBedrockBlock vanillaBedrockDefinition = blockStateOrderedMap.get(buildBedrockState(entry.getValue(), stateVersion, stateMapper));
GeyserBedrockBlock bedrockDefinition = blockStateOrderedMap.get(buildBedrockState(entry.getValue(), stateVersion, stateMapper));
if (bedrockDefinition == null) {
throw new RuntimeException("Unable to find " + javaId + " Bedrock BlockDefinition on version "
+ palette.getKey().key() + "! Built NBT tag: \n" + buildBedrockState(entry.getValue(), stateVersion, stateMapper));
GeyserBedrockBlock bedrockDefinition;
CustomBlockState blockStateOverride = BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get(javaRuntimeId);
if (blockStateOverride == null) {
bedrockDefinition = vanillaBedrockDefinition;
if (bedrockDefinition == null) {
throw new RuntimeException("Unable to find " + javaId + " Bedrock runtime ID! Built NBT tag: \n" +
palette.getKey().key() + buildBedrockState(entry.getValue(), stateVersion, stateMapper));
}
} else {
bedrockDefinition = customBlockStateDefinitions.get(blockStateOverride);
if (bedrockDefinition == null) {
throw new RuntimeException("Unable to find " + javaId + " Bedrock runtime ID! Custom block override: \n" +
blockStateOverride);
}
}
switch (javaId) {
@ -236,9 +322,10 @@ public final class BlockRegistryPopulator {
// Get the tag needed for non-empty flower pots
if (entry.getValue().get("pottable") != null) {
flowerPotBlocks.put(cleanJavaIdentifier.intern(), blocksTag.get(bedrockDefinition.getRuntimeId()));
flowerPotBlocks.put(cleanJavaIdentifier.intern(), blockStates.get(bedrockDefinition.getRuntimeId()));
}
javaToVanillaBedrockBlocks[javaRuntimeId] = vanillaBedrockDefinition;
javaToBedrockBlocks[javaRuntimeId] = bedrockDefinition;
}
@ -263,6 +350,33 @@ public final class BlockRegistryPopulator {
}
builder.bedrockMovingBlock(movingBlockDefinition);
Map<JavaBlockState, CustomBlockState> nonVanillaStateOverrides = BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get();
if (nonVanillaStateOverrides.size() > 0) {
// First ensure all non vanilla runtime IDs at minimum are air in case they aren't consecutive
Arrays.fill(javaToVanillaBedrockBlocks, minCustomRuntimeID, javaToVanillaBedrockBlocks.length, airDefinition);
Arrays.fill(javaToBedrockBlocks, minCustomRuntimeID, javaToBedrockBlocks.length, airDefinition);
for (Map.Entry<JavaBlockState, CustomBlockState> entry : nonVanillaStateOverrides.entrySet()) {
GeyserBedrockBlock bedrockDefinition = customBlockStateDefinitions.get(entry.getValue());
if (bedrockDefinition == null) {
GeyserImpl.getInstance().getLogger().warning("Unable to find custom block for " + entry.getValue());
continue;
}
JavaBlockState javaState = entry.getKey();
int stateRuntimeId = javaState.javaId();
boolean waterlogged = javaState.waterlogged();
if (waterlogged) {
BlockRegistries.WATERLOGGED.register(set -> set.set(stateRuntimeId));
}
javaToVanillaBedrockBlocks[stateRuntimeId] = bedrockDefinition; // TODO: Check this?
javaToBedrockBlocks[stateRuntimeId] = bedrockDefinition;
}
}
// Loop around again to find all item frame runtime IDs
Object2ObjectMaps.fastForEach(blockStateOrderedMap, entry -> {
String name = entry.getKey().getString("name");
@ -274,10 +388,15 @@ public final class BlockRegistryPopulator {
BlockRegistries.BLOCKS.register(palette.getKey().valueInt(), builder.blockStateVersion(stateVersion)
.bedrockRuntimeMap(bedrockRuntimeMap)
.javaToBedrockBlocks(javaToBedrockBlocks)
.javaToVanillaBedrockBlocks(javaToVanillaBedrockBlocks)
.stateDefinitionMap(blockStateOrderedMap)
.itemFrames(itemFrames)
.flowerPotBlocks(flowerPotBlocks)
.jigsawStates(jigsawDefinitions)
.remappedVanillaIds(remappedVanillaIds)
.blockProperties(customBlockProperties)
.customBlockStateDefinitions(customBlockStateDefinitions)
.extendedCollisionBoxes(extendedCollisionBoxes)
.build());
}
}
@ -290,7 +409,20 @@ public final class BlockRegistryPopulator {
throw new AssertionError("Unable to load Java block mappings", e);
}
BlockRegistries.JAVA_BLOCKS.set(new BlockMapping[blocksJson.size()]); // Set array size to number of blockstates
javaBlocksSize = blocksJson.size();
if (BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().size() > 0) {
minCustomRuntimeID = BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().keySet().stream().min(Comparator.comparing(JavaBlockState::javaId)).get().javaId();
maxCustomRuntimeID = BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().keySet().stream().max(Comparator.comparing(JavaBlockState::javaId)).get().javaId();
if (minCustomRuntimeID < blocksJson.size()) {
throw new RuntimeException("Non vanilla custom block state overrides runtime ID must start after the last vanilla block state (" + javaBlocksSize + ")");
}
javaBlocksSize = maxCustomRuntimeID + 1; // Runtime ids start at 0, so we need to add 1
}
BlockRegistries.JAVA_BLOCKS.set(new BlockMapping[javaBlocksSize]); // Set array size to number of blockstates
Deque<String> cleanIdentifiers = new ArrayDeque<>();
@ -428,6 +560,46 @@ public final class BlockRegistryPopulator {
}
BlockStateValues.JAVA_WATER_ID = waterRuntimeId;
if (BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().size() > 0) {
Set<Integer> usedNonVanillaRuntimeIDs = new HashSet<>();
for (JavaBlockState javaBlockState : BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().keySet()) {
if (!usedNonVanillaRuntimeIDs.add(javaBlockState.javaId())) {
throw new RuntimeException("Duplicate runtime ID " + javaBlockState.javaId() + " for non vanilla Java block state " + javaBlockState.identifier());
}
CustomBlockState customBlockState = BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.get().get(javaBlockState);
String javaId = javaBlockState.identifier();
int stateRuntimeId = javaBlockState.javaId();
BlockMapping blockMapping = BlockMapping.builder()
.canBreakWithHand(javaBlockState.canBreakWithHand())
.pickItem(javaBlockState.pickItem())
.isNonVanilla(true)
.javaIdentifier(javaId)
.javaBlockId(javaBlockState.stateGroupId())
.hardness(javaBlockState.blockHardness())
.pistonBehavior(javaBlockState.pistonBehavior() == null ? PistonBehavior.NORMAL : PistonBehavior.getByName(javaBlockState.pistonBehavior()))
.isBlockEntity(javaBlockState.hasBlockEntity())
.build();
String cleanJavaIdentifier = BlockUtils.getCleanIdentifier(javaBlockState.identifier());
String bedrockIdentifier = customBlockState.block().identifier();
if (!cleanJavaIdentifier.equals(cleanIdentifiers.peekLast())) {
uniqueJavaId++;
cleanIdentifiers.add(cleanJavaIdentifier.intern());
}
BlockRegistries.JAVA_IDENTIFIER_TO_ID.register(javaId, stateRuntimeId);
BlockRegistries.JAVA_BLOCKS.register(stateRuntimeId, blockMapping);
// Keeping this here since this is currently unchanged between versions
// It's possible to only have this store differences in names, but the key set of all Java names is used in sending command suggestions
BlockRegistries.JAVA_TO_BEDROCK_IDENTIFIERS.register(cleanJavaIdentifier.intern(), bedrockIdentifier.intern());
}
}
BlockRegistries.CLEAN_JAVA_IDENTIFIERS.set(cleanIdentifiers.toArray(new String[0]));
BLOCKS_JSON = blocksJson;
@ -481,4 +653,22 @@ public final class BlockRegistryPopulator {
tagBuilder.put("states", statesBuilder.build());
return tagBuilder.build();
}
private static final long FNV1_64_OFFSET_BASIS = 0xcbf29ce484222325L;
private static final long FNV1_64_PRIME = 1099511628211L;
/**
* Hashes a string using the FNV-1a 64-bit algorithm.
*
* @param str The string to hash
* @return The hashed string
*/
private static long fnv164(String str) {
long hash = FNV1_64_OFFSET_BASIS;
for (byte b : str.getBytes(StandardCharsets.UTF_8)) {
hash *= FNV1_64_PRIME;
hash ^= b;
}
return hash;
}
}

View File

@ -0,0 +1,480 @@
package org.geysermc.geyser.registry.populator;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.nbt.NbtType;
import org.cloudburstmc.protocol.bedrock.codec.v594.Bedrock_v594;
import org.cloudburstmc.protocol.bedrock.data.BlockPropertyData;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.block.custom.CustomBlockData;
import org.geysermc.geyser.api.block.custom.CustomBlockPermutation;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.api.block.custom.component.BoxComponent;
import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents;
import org.geysermc.geyser.api.block.custom.component.MaterialInstance;
import org.geysermc.geyser.api.block.custom.component.PlacementConditions;
import org.geysermc.geyser.api.block.custom.component.PlacementConditions.Face;
import org.geysermc.geyser.api.block.custom.nonvanilla.JavaBlockState;
import org.geysermc.geyser.api.block.custom.property.CustomBlockProperty;
import org.geysermc.geyser.api.block.custom.property.PropertyType;
import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCustomBlocksEvent;
import org.geysermc.geyser.api.util.CreativeCategory;
import org.geysermc.geyser.level.block.GeyserCustomBlockState;
import org.geysermc.geyser.level.block.GeyserCustomBlockComponents.CustomBlockComponentsBuilder;
import org.geysermc.geyser.level.block.GeyserCustomBlockData.CustomBlockDataBuilder;
import org.geysermc.geyser.level.block.GeyserGeometryComponent.GeometryComponentBuilder;
import org.geysermc.geyser.level.block.GeyserMaterialInstance.MaterialInstanceBuilder;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.mappings.MappingsConfigReader;
import org.geysermc.geyser.registry.type.CustomSkull;
import org.geysermc.geyser.util.MathUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class CustomBlockRegistryPopulator {
/**
* The stage of population
*/
public enum Stage {
DEFINITION,
VANILLA_REGISTRATION,
NON_VANILLA_REGISTRATION,
CUSTOM_REGISTRATION;
}
/**
* Populates the custom block registries by stage
*
* @param stage the stage to populate
*/
public static void populate(Stage stage) {
if (!GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) {
return;
}
switch (stage) {
case DEFINITION -> { populateBedrock(); }
case VANILLA_REGISTRATION -> { populateVanilla(); }
case NON_VANILLA_REGISTRATION -> { populateNonVanilla(); }
case CUSTOM_REGISTRATION -> { registration(); }
default -> { throw new IllegalArgumentException("Unknown stage: " + stage); }
}
}
private static Set<CustomBlockData> customBlocks;
private static Set<String> customBlockNames;
private static Int2ObjectMap<CustomBlockState> blockStateOverrides;
private static Map<String, CustomBlockData> customBlockItemOverrides;
private static Map<JavaBlockState, CustomBlockState> nonVanillaBlockStateOverrides;
/**
* Initializes custom blocks defined by API
*/
private static void populateBedrock() {
customBlocks = new ObjectOpenHashSet<>();
customBlockNames = new ObjectOpenHashSet<>();
blockStateOverrides = new Int2ObjectOpenHashMap<>();
customBlockItemOverrides = new HashMap<>();
nonVanillaBlockStateOverrides = new HashMap<>();
GeyserImpl.getInstance().getEventBus().fire(new GeyserDefineCustomBlocksEvent() {
@Override
public void register(@NonNull CustomBlockData customBlockData) {
if (customBlockData.name().length() == 0) {
throw new IllegalArgumentException("Custom block name must have at least 1 character.");
}
if (!customBlockNames.add(customBlockData.name())) {
throw new IllegalArgumentException("Another custom block was already registered under the name: " + customBlockData.name());
}
if (Character.isDigit(customBlockData.name().charAt(0))) {
throw new IllegalArgumentException("Custom block can not start with a digit. Name: " + customBlockData.name());
}
customBlocks.add(customBlockData);
}
@Override
public void registerOverride(@NonNull String javaIdentifier, @NonNull CustomBlockState customBlockState) {
int id = BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(javaIdentifier, -1);
if (id == -1) {
throw new IllegalArgumentException("Unknown Java block state. Identifier: " + javaIdentifier);
}
if (!customBlocks.contains(customBlockState.block())) {
throw new IllegalArgumentException("Custom block is unregistered. Name: " + customBlockState.name());
}
CustomBlockState oldBlockState = blockStateOverrides.put(id, customBlockState);
if (oldBlockState != null) {
GeyserImpl.getInstance().getLogger().debug("Duplicate block state override for Java Identifier: " +
javaIdentifier + " Old override: " + oldBlockState.name() + " New override: " + customBlockState.name());
}
}
@Override
public void registerItemOverride(@NonNull String javaIdentifier, @NonNull CustomBlockData customBlockData) {
if (!customBlocks.contains(customBlockData)) {
throw new IllegalArgumentException("Custom block is unregistered. Name: " + customBlockData.name());
}
customBlockItemOverrides.put(javaIdentifier, customBlockData);
}
@Override
public void registerOverride(@NonNull JavaBlockState javaBlockState, @NonNull CustomBlockState customBlockState) {
if (!customBlocks.contains(customBlockState.block())) {
throw new IllegalArgumentException("Custom block is unregistered. Name: " + customBlockState.name());
}
nonVanillaBlockStateOverrides.put(javaBlockState, customBlockState);
}
});
}
/**
* Registers all vanilla custom blocks and skulls defined by API and mappings
*/
private static void populateVanilla() {
for (CustomSkull customSkull : BlockRegistries.CUSTOM_SKULLS.get().values()) {
customBlocks.add(customSkull.getCustomBlockData());
}
Map<CustomBlockData, Set<Integer>> extendedCollisionBoxes = new HashMap<>();
Map<BoxComponent, CustomBlockData> extendedCollisionBoxSet = new HashMap<>();
MappingsConfigReader mappingsConfigReader = new MappingsConfigReader();
mappingsConfigReader.loadBlockMappingsFromJson((key, block) -> {
customBlocks.add(block.data());
if (block.overrideItem()) {
customBlockItemOverrides.put(block.javaIdentifier(), block.data());
}
block.states().forEach((javaIdentifier, customBlockState) -> {
int id = BlockRegistries.JAVA_IDENTIFIER_TO_ID.getOrDefault(javaIdentifier, -1);
blockStateOverrides.put(id, customBlockState.state());
BoxComponent extendedCollisionBox = customBlockState.extendedCollisionBox();
if (extendedCollisionBox != null) {
CustomBlockData extendedCollisionBlock = extendedCollisionBoxSet.computeIfAbsent(extendedCollisionBox, box -> {
CustomBlockData collisionBlock = createExtendedCollisionBlock(box, extendedCollisionBoxSet.size());
customBlocks.add(collisionBlock);
return collisionBlock;
});
extendedCollisionBoxes.computeIfAbsent(extendedCollisionBlock, k -> new HashSet<>())
.add(id);
}
});
});
BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.set(blockStateOverrides);
GeyserImpl.getInstance().getLogger().info("Registered " + blockStateOverrides.size() + " custom block overrides.");
BlockRegistries.CUSTOM_BLOCK_ITEM_OVERRIDES.set(customBlockItemOverrides);
GeyserImpl.getInstance().getLogger().info("Registered " + customBlockItemOverrides.size() + " custom block item overrides.");
BlockRegistries.EXTENDED_COLLISION_BOXES.set(extendedCollisionBoxes);
GeyserImpl.getInstance().getLogger().info("Registered " + extendedCollisionBoxes.size() + " custom block extended collision boxes.");
}
/**
* Registers all non-vanilla custom blocks defined by API
*/
private static void populateNonVanilla() {
BlockRegistries.NON_VANILLA_BLOCK_STATE_OVERRIDES.set(nonVanillaBlockStateOverrides);
GeyserImpl.getInstance().getLogger().info("Registered " + nonVanillaBlockStateOverrides.size() + " non-vanilla block overrides.");
}
/**
* Registers all bedrock custom blocks defined in previous stages
*/
private static void registration() {
BlockRegistries.CUSTOM_BLOCKS.set(customBlocks.toArray(new CustomBlockData[0]));
GeyserImpl.getInstance().getLogger().info("Registered " + customBlocks.size() + " custom blocks.");
}
/**
* Generates and appends all custom block states to the provided list of custom block states
* Appends the custom block states to the provided list of NBT maps
*
* @param customBlock the custom block data to generate states for
* @param blockStates the list of NBT maps to append the custom block states to
* @param customExtBlockStates the list of custom block states to append the custom block states to
* @param stateVersion the state version to use for the custom block states
*/
static void generateCustomBlockStates(CustomBlockData customBlock, List<NbtMap> blockStates, List<CustomBlockState> customExtBlockStates, int stateVersion) {
int totalPermutations = 1;
for (CustomBlockProperty<?> property : customBlock.properties().values()) {
totalPermutations *= property.values().size();
}
for (int i = 0; i < totalPermutations; i++) {
NbtMapBuilder statesBuilder = NbtMap.builder();
int permIndex = i;
for (CustomBlockProperty<?> property : customBlock.properties().values()) {
statesBuilder.put(property.name(), property.values().get(permIndex % property.values().size()));
permIndex /= property.values().size();
}
NbtMap states = statesBuilder.build();
blockStates.add(NbtMap.builder()
.putString("name", customBlock.identifier())
.putInt("version", stateVersion)
.putCompound("states", states)
.build());
customExtBlockStates.add(new GeyserCustomBlockState(customBlock, states));
}
}
/**
* Generates and returns the block property data for the provided custom block
*
* @param customBlock the custom block to generate block property data for
* @param protocolVersion the protocol version to use for the block property data
* @return the block property data for the provided custom block
*/
@SuppressWarnings("unchecked")
static BlockPropertyData generateBlockPropertyData(CustomBlockData customBlock, int protocolVersion) {
List<NbtMap> permutations = new ArrayList<>();
for (CustomBlockPermutation permutation : customBlock.permutations()) {
permutations.add(NbtMap.builder()
.putCompound("components", CustomBlockRegistryPopulator.convertComponents(permutation.components(), protocolVersion))
.putString("condition", permutation.condition())
.build());
}
// The order that properties are defined influences the order that block states are generated
List<NbtMap> properties = new ArrayList<>();
for (CustomBlockProperty<?> property : customBlock.properties().values()) {
NbtMapBuilder propertyBuilder = NbtMap.builder()
.putString("name", property.name());
if (property.type() == PropertyType.booleanProp()) {
propertyBuilder.putList("enum", NbtType.BYTE, List.of((byte) 0, (byte) 1));
} else if (property.type() == PropertyType.integerProp()) {
propertyBuilder.putList("enum", NbtType.INT, (List<Integer>) property.values());
} else if (property.type() == PropertyType.stringProp()) {
propertyBuilder.putList("enum", NbtType.STRING, (List<String>) property.values());
}
properties.add(propertyBuilder.build());
}
CreativeCategory creativeCategory = customBlock.creativeCategory() != null ? customBlock.creativeCategory() : CreativeCategory.NONE;
String creativeGroup = customBlock.creativeGroup() != null ? customBlock.creativeGroup() : "";
NbtMap propertyTag = NbtMap.builder()
.putCompound("components", CustomBlockRegistryPopulator.convertComponents(customBlock.components(), protocolVersion))
// this is required or the client will crash
// in the future, this can be used to replace items in the creative inventory
// this would require us to map https://wiki.bedrock.dev/documentation/creative-categories.html#for-blocks programatically
.putCompound("menu_category", NbtMap.builder()
.putString("category", creativeCategory.internalName())
.putString("group", creativeGroup)
.putBoolean("is_hidden_in_commands", false)
.build())
// meaning of this version is unknown, but it's required for tags to work and should probably be checked periodically
.putInt("molangVersion", 1)
.putList("permutations", NbtType.COMPOUND, permutations)
.putList("properties", NbtType.COMPOUND, properties)
.build();
return new BlockPropertyData(customBlock.identifier(), propertyTag);
}
/**
* Converts the provided custom block components to an {@link NbtMap} to be sent to the client in the StartGame packet
*
* @param components the custom block components to convert
* @param protocolVersion the protocol version to use for the conversion
* @return the NBT representation of the provided custom block components
*/
private static NbtMap convertComponents(CustomBlockComponents components, int protocolVersion) {
if (components == null) {
return NbtMap.EMPTY;
}
NbtMapBuilder builder = NbtMap.builder();
if (components.displayName() != null) {
builder.putCompound("minecraft:display_name", NbtMap.builder()
.putString("value", components.displayName())
.build());
}
if (components.selectionBox() != null) {
builder.putCompound("minecraft:selection_box", convertBox(components.selectionBox()));
}
if (components.collisionBox() != null) {
builder.putCompound("minecraft:collision_box", convertBox(components.collisionBox()));
}
if (components.geometry() != null) {
NbtMapBuilder geometryBuilder = NbtMap.builder();
if (protocolVersion >= Bedrock_v594.CODEC.getProtocolVersion()) {
geometryBuilder.putString("identifier", components.geometry().identifier());
if (components.geometry().boneVisibility() != null) {
NbtMapBuilder boneVisibilityBuilder = NbtMap.builder();
components.geometry().boneVisibility().entrySet().forEach(
entry -> boneVisibilityBuilder.putString(entry.getKey(), entry.getValue()));
geometryBuilder.putCompound("bone_visibility", boneVisibilityBuilder.build());
}
} else {
geometryBuilder.putString("value", components.geometry().identifier());
}
builder.putCompound("minecraft:geometry", geometryBuilder.build());
}
if (!components.materialInstances().isEmpty()) {
NbtMapBuilder materialsBuilder = NbtMap.builder();
for (Map.Entry<String, MaterialInstance> entry : components.materialInstances().entrySet()) {
MaterialInstance materialInstance = entry.getValue();
materialsBuilder.putCompound(entry.getKey(), NbtMap.builder()
.putString("texture", materialInstance.texture())
.putString("render_method", materialInstance.renderMethod())
.putBoolean("face_dimming", materialInstance.faceDimming())
.putBoolean("ambient_occlusion", materialInstance.faceDimming())
.build());
}
builder.putCompound("minecraft:material_instances", NbtMap.builder()
// we could read these, but there is no functional reason to use them at the moment
// they only allow you to make aliases for material instances
// but you could already just define the same instance twice if this was really needed
.putCompound("mappings", NbtMap.EMPTY)
.putCompound("materials", materialsBuilder.build())
.build());
}
if (components.placementFilter() != null) {
builder.putCompound("minecraft:placement_filter", NbtMap.builder()
.putList("conditions", NbtType.COMPOUND, convertPlacementFilter(components.placementFilter()))
.build());
}
if (components.destructibleByMining() != null) {
builder.putCompound("minecraft:destructible_by_mining", NbtMap.builder()
.putFloat("value", components.destructibleByMining())
.build());
}
if (components.friction() != null) {
builder.putCompound("minecraft:friction", NbtMap.builder()
.putFloat("value", components.friction())
.build());
}
if (components.lightEmission() != null) {
builder.putCompound("minecraft:light_emission", NbtMap.builder()
.putByte("emission", components.lightEmission().byteValue())
.build());
}
if (components.lightDampening() != null) {
builder.putCompound("minecraft:light_dampening", NbtMap.builder()
.putByte("lightLevel", components.lightDampening().byteValue())
.build());
}
if (components.transformation() != null) {
builder.putCompound("minecraft:transformation", NbtMap.builder()
.putInt("RX", MathUtils.unwrapDegreesToInt(components.transformation().rx()) / 90)
.putInt("RY", MathUtils.unwrapDegreesToInt(components.transformation().ry()) / 90)
.putInt("RZ", MathUtils.unwrapDegreesToInt(components.transformation().rz()) / 90)
.putFloat("SX", components.transformation().sx())
.putFloat("SY", components.transformation().sy())
.putFloat("SZ", components.transformation().sz())
.putFloat("TX", components.transformation().tx())
.putFloat("TY", components.transformation().ty())
.putFloat("TZ", components.transformation().tz())
.build());
}
if (components.unitCube()) {
builder.putCompound("minecraft:unit_cube", NbtMap.EMPTY);
}
// place_air is not an actual component
// We just apply a dummy event to prevent the client from trying to place a block
// This mitigates the issue with the client sometimes double placing blocks
if (components.placeAir()) {
builder.putCompound("minecraft:on_player_placing", NbtMap.builder()
.putString("triggerType", "geyser:place_event")
.build());
}
if (!components.tags().isEmpty()) {
components.tags().forEach(tag -> builder.putCompound("tag:" + tag, NbtMap.EMPTY));
}
return builder.build();
}
/**
* Converts the provided box component to an {@link NbtMap}
*
* @param boxComponent the box component to convert
* @return the NBT representation of the provided box component
*/
private static NbtMap convertBox(BoxComponent boxComponent) {
return NbtMap.builder()
.putBoolean("enabled", !boxComponent.isEmpty())
.putList("origin", NbtType.FLOAT, boxComponent.originX(), boxComponent.originY(), boxComponent.originZ())
.putList("size", NbtType.FLOAT, boxComponent.sizeX(), boxComponent.sizeY(), boxComponent.sizeZ())
.build();
}
/**
* Converts the provided placement filter to a list of {@link NbtMap}
*
* @param placementFilter the placement filter to convert
* @return the NBT representation of the provided placement filter
*/
private static List<NbtMap> convertPlacementFilter(List<PlacementConditions> placementFilter) {
List<NbtMap> conditions = new ArrayList<>();
placementFilter.forEach((condition) -> {
NbtMapBuilder conditionBuilder = NbtMap.builder();
// allowed_faces on the network is represented by 6 bits for the 6 possible faces
// the enum has the proper values for that face only, so we just bitwise OR them together
byte allowedFaces = 0;
for (Face face : condition.allowedFaces()) { allowedFaces |= (1 << face.ordinal()); }
conditionBuilder.putByte("allowed_faces", allowedFaces);
// block_filters is a list of either blocks or queries for block tags
// if these match the block the player is trying to place on, the placement is allowed by the client
List <NbtMap> blockFilters = new ArrayList<>();
condition.blockFilters().forEach((value, type) -> {
NbtMapBuilder blockFilterBuilder = NbtMap.builder();
switch (type) {
case BLOCK -> blockFilterBuilder.putString("name", value);
// meaning of this version is unknown, but it's required for tags to work and should probably be checked periodically
case TAG -> blockFilterBuilder.putString("tags", value).putInt("tags_version", 6);
}
blockFilters.add(blockFilterBuilder.build());
});
conditionBuilder.putList("block_filters", NbtType.COMPOUND, blockFilters);
conditions.add(conditionBuilder.build());
});
return conditions;
}
private static CustomBlockData createExtendedCollisionBlock(BoxComponent boxComponent, int extendedCollisionBlock) {
CustomBlockData customBlockData = new CustomBlockDataBuilder()
.name("extended_collision_" + extendedCollisionBlock)
.components(
new CustomBlockComponentsBuilder()
.collisionBox(boxComponent)
.selectionBox(BoxComponent.emptyBox())
.materialInstance("*", new MaterialInstanceBuilder()
.texture("glass")
.renderMethod("alpha_test")
.faceDimming(false)
.ambientOcclusion(false)
.build())
.lightDampening(0)
.geometry(new GeometryComponentBuilder()
.identifier("geometry.invisible")
.build())
.build())
.build();
return customBlockData;
}
}

View File

@ -41,10 +41,9 @@ import org.geysermc.geyser.api.util.TriState;
import org.geysermc.geyser.event.type.GeyserDefineCustomItemsEventImpl;
import org.geysermc.geyser.item.GeyserCustomMappingData;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.item.components.ToolBreakSpeedsUtils;
import org.geysermc.geyser.item.components.WearableSlot;
import org.geysermc.geyser.item.mappings.MappingsConfigReader;
import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.registry.mappings.MappingsConfigReader;
import org.geysermc.geyser.registry.type.GeyserMappingItem;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.registry.type.NonVanillaItemRegistration;
@ -56,7 +55,7 @@ public class CustomItemRegistryPopulator {
public static void populate(Map<String, GeyserMappingItem> items, Multimap<String, CustomItemData> customItems, List<NonVanillaCustomItemData> nonVanillaCustomItems) {
MappingsConfigReader mappingsConfigReader = new MappingsConfigReader();
// Load custom items from mappings files
mappingsConfigReader.loadMappingsFromJson((key, item) -> {
mappingsConfigReader.loadItemMappingsFromJson((key, item) -> {
if (CustomItemRegistryPopulator.initialCheck(key, item, items)) {
customItems.get(key).add(item);
}
@ -294,34 +293,45 @@ public class CustomItemRegistryPopulator {
boolean canDestroyInCreative = true;
float miningSpeed = 1.0f;
if (toolType.equals("shears")) {
componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getShearsDigger(15));
} else {
int toolSpeed = ToolBreakSpeedsUtils.toolTierToSpeed(toolTier);
switch (toolType) {
case "sword" -> {
miningSpeed = 1.5f;
canDestroyInCreative = false;
componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getSwordDigger(toolSpeed));
componentBuilder.putCompound("minecraft:weapon", NbtMap.EMPTY);
}
case "pickaxe" -> {
componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getPickaxeDigger(toolSpeed, toolTier));
setItemTag(componentBuilder, "pickaxe");
}
case "axe" -> {
componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getAxeDigger(toolSpeed));
setItemTag(componentBuilder, "axe");
}
case "shovel" -> {
componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getShovelDigger(toolSpeed));
setItemTag(componentBuilder, "shovel");
}
case "hoe" -> {
componentBuilder.putCompound("minecraft:digger", ToolBreakSpeedsUtils.getHoeDigger(toolSpeed));
setItemTag(componentBuilder, "hoe");
}
}
// This means client side the tool can never destroy a block
// This works because the molang '1' for tags will be true for all blocks and the speed will be 0
// We want this since we calculate break speed server side in BedrockActionTranslator
List<NbtMap> speed = new ArrayList<>(List.of(
NbtMap.builder()
.putCompound("block", NbtMap.builder()
.putString("tags", "1")
.build())
.putCompound("on_dig", NbtMap.builder()
.putCompound("condition", NbtMap.builder()
.putString("expression", "")
.putInt("version", -1)
.build())
.putString("event", "tool_durability")
.putString("target", "self")
.build())
.putInt("speed", 0)
.build()
));
componentBuilder.putCompound("minecraft:digger",
NbtMap.builder()
.putList("destroy_speeds", NbtType.COMPOUND, speed)
.putCompound("on_dig", NbtMap.builder()
.putCompound("condition", NbtMap.builder()
.putString("expression", "")
.putInt("version", -1)
.build())
.putString("event", "tool_durability")
.putString("target", "self")
.build())
.putBoolean("use_efficiency", true)
.build()
);
if (toolType.equals("sword")) {
miningSpeed = 1.5f;
canDestroyInCreative = false;
componentBuilder.putCompound("minecraft:weapon", NbtMap.EMPTY);
}
itemProperties.putBoolean("hand_equipped", true);

View File

@ -0,0 +1,187 @@
/*
* 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.registry.populator;
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.NonNull;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.event.lifecycle.GeyserDefineCustomSkullsEvent;
import org.geysermc.geyser.configuration.GeyserCustomSkullConfiguration;
import org.geysermc.geyser.pack.SkullResourcePackManager;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.type.CustomSkull;
import org.geysermc.geyser.skin.SkinManager;
import org.geysermc.geyser.skin.SkinProvider;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.util.FileUtils;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.function.Function;
public class CustomSkullRegistryPopulator {
public static void populate() {
SkullResourcePackManager.SKULL_SKINS.clear(); // Remove skins after reloading
BlockRegistries.CUSTOM_SKULLS.set(Object2ObjectMaps.emptyMap());
if (!GeyserImpl.getInstance().getConfig().isAddNonBedrockItems()) {
return;
}
GeyserCustomSkullConfiguration skullConfig;
try {
GeyserBootstrap bootstrap = GeyserImpl.getInstance().getBootstrap();
Path skullConfigPath = bootstrap.getConfigFolder().resolve("custom-skulls.yml");
File skullConfigFile = FileUtils.fileOrCopiedFromResource(skullConfigPath.toFile(), "custom-skulls.yml", Function.identity(), bootstrap);
skullConfig = FileUtils.loadConfig(skullConfigFile, GeyserCustomSkullConfiguration.class);
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.config.failed"), e);
return;
}
BlockRegistries.CUSTOM_SKULLS.set(new Object2ObjectOpenHashMap<>());
List<String> profiles = new ArrayList<>(skullConfig.getPlayerProfiles());
List<String> usernames = new ArrayList<>(skullConfig.getPlayerUsernames());
List<String> uuids = new ArrayList<>(skullConfig.getPlayerUUIDs());
List<String> skinHashes = new ArrayList<>(skullConfig.getPlayerSkinHashes());
GeyserImpl.getInstance().getEventBus().fire(new GeyserDefineCustomSkullsEvent() {
@Override
public void register(@NonNull String texture, @NonNull SkullTextureType type) {
switch (type) {
case USERNAME -> usernames.add(texture);
case UUID -> uuids.add(texture);
case PROFILE -> profiles.add(texture);
case SKIN_HASH -> skinHashes.add(texture);
}
}
});
usernames.forEach((username) -> {
String profile = getProfileFromUsername(username);
if (profile != null) {
String skinHash = getSkinHash(profile);
if (skinHash != null) {
skinHashes.add(skinHash);
}
}
});
uuids.forEach((uuid) -> {
String profile = getProfileFromUuid(uuid);
if (profile != null) {
String skinHash = getSkinHash(profile);
if (skinHash != null) {
skinHashes.add(skinHash);
}
}
});
profiles.forEach((profile) -> {
String skinHash = getSkinHash(profile);
if (skinHash != null) {
skinHashes.add(skinHash);
}
});
skinHashes.forEach((skinHash) -> {
if (!skinHash.matches("^[a-fA-F0-9]+$")) {
GeyserImpl.getInstance().getLogger().error("Skin hash " + skinHash + " does not match required format ^[a-fA-F0-9]{64}$ and will not be added as a custom block.");
return;
}
try {
SkullResourcePackManager.cacheSkullSkin(skinHash);
BlockRegistries.CUSTOM_SKULLS.register(skinHash, new CustomSkull(skinHash));
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().error("Failed to cache skin for skull texture " + skinHash + " This skull will not be added as a custom block.", e);
}
});
GeyserImpl.getInstance().getLogger().info("Registered " + BlockRegistries.CUSTOM_SKULLS.get().size() + " custom skulls as custom blocks.");
}
/**
* Gets the skin hash from a base64 encoded profile
* @param profile the base64 encoded profile
* @return the skin hash or null if the profile is invalid
*/
private static String getSkinHash(String profile) {
try {
SkinManager.GameProfileData profileData = SkinManager.GameProfileData.loadFromJson(profile);
if (profileData == null) {
GeyserImpl.getInstance().getLogger().warning("Skull texture " + profile + " contained no skins and will not be added as a custom block.");
return null;
}
String skinUrl = profileData.skinUrl();
return skinUrl.substring(skinUrl.lastIndexOf("/") + 1);
} catch (IOException e) {
GeyserImpl.getInstance().getLogger().error("Skull texture " + profile + " is invalid and will not be added as a custom block.", e);
return null;
}
}
/**
* Gets the base64 encoded profile from a player's username
* @param username the player username
* @return the base64 encoded profile or null if the request failed
*/
private static String getProfileFromUsername(String username) {
try {
return SkinProvider.requestTexturesFromUsername(username).get();
} catch (InterruptedException | ExecutionException e) {
GeyserImpl.getInstance().getLogger().error("Unable to request skull textures for " + username + " This skull will not be added as a custom block.", e);
return null;
}
}
/**
* Gets the base64 encoded profile from a player's UUID
* @param uuid the player UUID
* @return the base64 encoded profile or null if the request failed
*/
private static String getProfileFromUuid(String uuid) {
try {
String uuidDigits = uuid.replace("-", "");
if (uuidDigits.length() != 32) {
GeyserImpl.getInstance().getLogger().error("Invalid skull uuid " + uuid + " This skull will not be added as a custom block.");
return null;
}
return SkinProvider.requestTexturesFromUUID(uuid).get();
} catch (InterruptedException | ExecutionException e) {
GeyserImpl.getInstance().getLogger().error("Unable to request skull textures for " + uuid + " This skull will not be added as a custom block.", e);
return null;
}
}
}

View File

@ -34,6 +34,7 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.objects.*;
import org.geysermc.geyser.Constants;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
@ -49,15 +50,18 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.ComponentItemData;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.geysermc.geyser.GeyserBootstrap;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.block.custom.CustomBlockData;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.api.block.custom.NonVanillaCustomBlockData;
import org.geysermc.geyser.api.item.custom.CustomItemData;
import org.geysermc.geyser.api.item.custom.CustomItemOptions;
import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData;
import org.geysermc.geyser.inventory.item.StoredItemMappings;
import org.geysermc.geyser.item.GeyserCustomMappingData;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.registry.type.*;
import java.io.InputStream;
@ -176,6 +180,8 @@ public class ItemRegistryPopulator {
Object2ObjectMap<String, BlockDefinition> bedrockBlockIdOverrides = new Object2ObjectOpenHashMap<>();
Object2IntMap<String> blacklistedIdentifiers = new Object2IntOpenHashMap<>();
Object2ObjectMap<CustomBlockData, ItemDefinition> customBlockItemDefinitions = new Object2ObjectOpenHashMap<>();
List<ItemDefinition> buckets = new ObjectArrayList<>();
List<ItemData> carpets = new ObjectArrayList<>();
@ -249,15 +255,29 @@ public class ItemRegistryPopulator {
BlockDefinition bedrockBlock = null;
Integer firstBlockRuntimeId = entry.getValue().getFirstBlockRuntimeId();
BlockDefinition customBlockItemOverride = null;
if (firstBlockRuntimeId != null) {
BlockDefinition blockOverride = bedrockBlockIdOverrides.get(bedrockIdentifier);
if (blockOverride != null) {
// We'll do this here for custom blocks we want in the creative inventory so we can piggyback off the existing logic to find these
// blocks in creativeItems
CustomBlockData customBlockData = BlockRegistries.CUSTOM_BLOCK_ITEM_OVERRIDES.getOrDefault(javaItem.javaIdentifier(), null);
if (customBlockData != null) {
// this block has a custom item override and thus we should use its runtime ID for the ItemMapping
if (customBlockData.includedInCreativeInventory()) {
CustomBlockState customBlockState = customBlockData.defaultBlockState();
customBlockItemOverride = blockMappings.getCustomBlockStateDefinitions().getOrDefault(customBlockState, null);
}
}
// If it' s a custom block we can't do this because we need to make sure we find the creative item
if (blockOverride != null && customBlockItemOverride == null) {
// Straight from BDS is our best chance of getting an item that doesn't run into issues
bedrockBlock = blockOverride;
} else {
// Try to get an example block runtime ID from the creative contents packet, for Bedrock identifier obtaining
int aValidBedrockBlockId = blacklistedIdentifiers.getOrDefault(bedrockIdentifier, -1);
if (aValidBedrockBlockId == -1) {
int aValidBedrockBlockId = blacklistedIdentifiers.getOrDefault(bedrockIdentifier, customBlockItemOverride != null ? customBlockItemOverride.getRuntimeId() : -1);
if (aValidBedrockBlockId == -1 && customBlockItemOverride == null) {
// Fallback
bedrockBlock = blockMappings.getBedrockBlock(firstBlockRuntimeId);
} else {
@ -273,7 +293,7 @@ public class ItemRegistryPopulator {
// and the last, if relevant. We then iterate over all those values and get their Bedrock equivalents
Integer lastBlockRuntimeId = entry.getValue().getLastBlockRuntimeId() == null ? firstBlockRuntimeId : entry.getValue().getLastBlockRuntimeId();
for (int i = firstBlockRuntimeId; i <= lastBlockRuntimeId; i++) {
GeyserBedrockBlock bedrockBlockRuntimeId = blockMappings.getBedrockBlock(i);
GeyserBedrockBlock bedrockBlockRuntimeId = blockMappings.getVanillaBedrockBlock(i);
NbtMap blockTag = bedrockBlockRuntimeId.getState();
String bedrockName = blockTag.getString("name");
if (!bedrockName.equals(correctBedrockIdentifier)) {
@ -339,6 +359,12 @@ public class ItemRegistryPopulator {
// Because we have replaced the Bedrock block ID, we also need to replace the creative contents block runtime ID
// That way, creative items work correctly for these blocks
// Set our custom block override now if there is one
if (customBlockItemOverride != null) {
bedrockBlock = customBlockItemOverride;
}
for (int j = 0; j < creativeItems.size(); j++) {
ItemData itemData = creativeItems.get(j);
if (itemData.getDefinition().equals(definition)) {
@ -347,16 +373,35 @@ public class ItemRegistryPopulator {
}
NbtMap states = ((GeyserBedrockBlock) itemData.getBlockDefinition()).getState().getCompound("states");
boolean valid = true;
for (Map.Entry<String, Object> nbtEntry : requiredBlockStates.entrySet()) {
if (!states.get(nbtEntry.getKey()).equals(nbtEntry.getValue())) {
if (states.getOrDefault(nbtEntry.getKey(), null) == null || !states.get(nbtEntry.getKey()).equals(nbtEntry.getValue())) {
// A required block state doesn't match - this one is not valid
valid = false;
break;
}
}
if (valid) {
creativeItems.set(j, itemData.toBuilder().blockDefinition(bedrockBlock).build());
if (customBlockItemOverride != null && customBlockData != null) {
// Assuming this is a valid custom block override we'll just register it now while we have the creative item
int customProtocolId = nextFreeBedrockId++;
mappingItem.setBedrockData(customProtocolId);
bedrockIdentifier = customBlockData.identifier();
definition = new SimpleItemDefinition(bedrockIdentifier, customProtocolId, true);
registry.put(customProtocolId, definition);
customBlockItemDefinitions.put(customBlockData, definition);
customIdMappings.put(customProtocolId, bedrockIdentifier);
creativeItems.set(j, itemData.toBuilder()
.definition(definition)
.blockDefinition(bedrockBlock)
.netId(itemData.getNetId())
.count(1)
.build());
} else {
creativeItems.set(j, itemData.toBuilder().blockDefinition(bedrockBlock).build());
}
break;
}
}
@ -397,10 +442,10 @@ public class ItemRegistryPopulator {
for (CustomItemData customItem : customItemsToLoad) {
int customProtocolId = nextFreeBedrockId++;
String customItemName = "geyser_custom:" + customItem.name();
String customItemName = customItem instanceof NonVanillaCustomItemData nonVanillaItem ? nonVanillaItem.identifier() : Constants.GEYSER_CUSTOM_NAMESPACE + ":" + customItem.name();
if (!registeredItemNames.add(customItemName)) {
if (firstMappingsPass) {
GeyserImpl.getInstance().getLogger().error("Custom item name '" + customItem.name() + "' already exists and was registered again! Skipping...");
GeyserImpl.getInstance().getLogger().error("Custom item name '" + customItemName + "' already exists and was registered again! Skipping...");
}
continue;
}
@ -516,6 +561,41 @@ public class ItemRegistryPopulator {
}
}
// Register the item forms of custom blocks
if (BlockRegistries.CUSTOM_BLOCKS.get().length != 0) {
for (CustomBlockData customBlock : BlockRegistries.CUSTOM_BLOCKS.get()) {
// We might've registered it already with the vanilla blocks so check first
if (customBlockItemDefinitions.containsKey(customBlock)) {
continue;
}
// Non-vanilla custom blocks will be handled in the item
// registry, so we don't need to do anything here.
if (customBlock instanceof NonVanillaCustomBlockData) {
continue;
}
int customProtocolId = nextFreeBedrockId++;
String identifier = customBlock.identifier();
final ItemDefinition definition = new SimpleItemDefinition(identifier, customProtocolId, true);
registry.put(customProtocolId, definition);
customBlockItemDefinitions.put(customBlock, definition);
customIdMappings.put(customProtocolId, identifier);
GeyserBedrockBlock bedrockBlock = blockMappings.getCustomBlockStateDefinitions().getOrDefault(customBlock.defaultBlockState(), null);
if (bedrockBlock != null && customBlock.includedInCreativeInventory()) {
creativeItems.add(ItemData.builder()
.definition(definition)
.blockDefinition(bedrockBlock)
.netId(creativeNetId.incrementAndGet())
.count(1)
.build());
}
}
}
ItemMappings itemMappings = ItemMappings.builder()
.items(mappings.toArray(new ItemMapping[0]))
.creativeItems(creativeItems.toArray(new ItemData[0]))
@ -527,6 +607,7 @@ public class ItemRegistryPopulator {
.componentItemData(componentItemData)
.lodestoneCompass(lodestoneEntry)
.customIdMappings(customIdMappings)
.customBlockItemDefinitions(customBlockItemDefinitions)
.build();
Registries.ITEMS.register(palette.protocolVersion(), itemMappings);

View File

@ -56,6 +56,7 @@ public class BlockMapping {
@Nonnull
PistonBehavior pistonBehavior;
boolean isBlockEntity;
boolean isNonVanilla;
/**
* @return the identifier without the additional block states

View File

@ -25,12 +25,17 @@
package org.geysermc.geyser.registry.type;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import lombok.Builder;
import lombok.Value;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.protocol.bedrock.data.BlockPropertyData;
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
import org.cloudburstmc.protocol.common.DefinitionRegistry;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import java.util.List;
import java.util.Map;
import java.util.Set;
@ -44,9 +49,11 @@ public class BlockMappings implements DefinitionRegistry<GeyserBedrockBlock> {
int blockStateVersion;
GeyserBedrockBlock[] javaToBedrockBlocks;
GeyserBedrockBlock[] javaToVanillaBedrockBlocks;
Map<NbtMap, GeyserBedrockBlock> stateDefinitionMap;
GeyserBedrockBlock[] bedrockRuntimeMap;
int[] remappedVanillaIds;
BlockDefinition commandBlock;
@ -55,6 +62,10 @@ public class BlockMappings implements DefinitionRegistry<GeyserBedrockBlock> {
Set<BlockDefinition> jigsawStates;
List<BlockPropertyData> blockProperties;
Object2ObjectMap<CustomBlockState, GeyserBedrockBlock> customBlockStateDefinitions;
Int2ObjectMap<GeyserBedrockBlock> extendedCollisionBoxes;
public int getBedrockBlockId(int javaState) {
return getBedrockBlock(javaState).getRuntimeId();
}
@ -66,6 +77,13 @@ public class BlockMappings implements DefinitionRegistry<GeyserBedrockBlock> {
return this.javaToBedrockBlocks[javaState];
}
public GeyserBedrockBlock getVanillaBedrockBlock(int javaState) {
if (javaState < 0 || javaState >= this.javaToVanillaBedrockBlocks.length) {
return bedrockAir;
}
return this.javaToVanillaBedrockBlocks[javaState];
}
public BlockDefinition getItemFrame(NbtMap tag) {
return this.itemFrames.get(tag);
}

View File

@ -0,0 +1,166 @@
/*
* 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.registry.type;
import lombok.Data;
import org.geysermc.geyser.api.block.custom.CustomBlockData;
import org.geysermc.geyser.api.block.custom.CustomBlockPermutation;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.api.block.custom.component.BoxComponent;
import org.geysermc.geyser.api.block.custom.component.CustomBlockComponents;
import org.geysermc.geyser.api.block.custom.component.TransformationComponent;
import org.geysermc.geyser.level.block.GeyserCustomBlockComponents;
import org.geysermc.geyser.level.block.GeyserCustomBlockData;
import org.geysermc.geyser.level.block.GeyserGeometryComponent.GeometryComponentBuilder;
import org.geysermc.geyser.level.block.GeyserMaterialInstance.MaterialInstanceBuilder;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
@Data
public class CustomSkull {
private final String skinHash;
private CustomBlockData customBlockData;
private static final String BITS_A_PROPERTY = "geyser_skull:bits_a";
private static final String BITS_B_PROPERTY = "geyser_skull:bits_b";
private static final int[] ROTATIONS = {0, -90, 180, 90};
private static final BoxComponent FLOOR_BOX = new BoxComponent(
-4, 0, -4,
8, 8, 8
);
private static final BoxComponent WALL_BOX = new BoxComponent(
-4, 4, 0,
8, 8, 8
);
public CustomSkull(String skinHash) {
this.skinHash = skinHash;
CustomBlockComponents components = new GeyserCustomBlockComponents.CustomBlockComponentsBuilder()
.destructibleByMining(1.5f)
.materialInstance("*", new MaterialInstanceBuilder()
.texture("geyser." + skinHash + "_player_skin")
.renderMethod("alpha_test")
.faceDimming(true)
.ambientOcclusion(true)
.build())
.lightDampening(0)
.placeAir(true)
.build();
List<CustomBlockPermutation> permutations = new ArrayList<>();
addDefaultPermutation(permutations);
addFloorPermutations(permutations);
addWallPermutations(permutations);
customBlockData = new GeyserCustomBlockData.CustomBlockDataBuilder()
.name("player_skull_" + skinHash)
.components(components)
.intProperty(BITS_A_PROPERTY, IntStream.rangeClosed(0, 6).boxed().toList()) // This gives us exactly 21 block states
.intProperty(BITS_B_PROPERTY, IntStream.rangeClosed(0, 2).boxed().toList())
.permutations(permutations)
.build();
}
public CustomBlockState getWallBlockState(int wallDirection) {
wallDirection = switch (wallDirection) {
case 0 -> 2; // South
case 90 -> 3; // West
case 180 -> 0; // North
case 270 -> 1; // East
default -> throw new IllegalArgumentException("Unknown skull wall direction: " + wallDirection);
};
return customBlockData.blockStateBuilder()
.intProperty(BITS_A_PROPERTY, wallDirection + 1)
.intProperty(BITS_B_PROPERTY, 0)
.build();
}
public CustomBlockState getFloorBlockState(int floorRotation) {
return customBlockData.blockStateBuilder()
.intProperty(BITS_A_PROPERTY, (5 + floorRotation) % 7)
.intProperty(BITS_B_PROPERTY, (5 + floorRotation) / 7)
.build();
}
private void addDefaultPermutation(List<CustomBlockPermutation> permutations) {
CustomBlockComponents components = new GeyserCustomBlockComponents.CustomBlockComponentsBuilder()
.geometry(new GeometryComponentBuilder()
.identifier("geometry.geyser.player_skull_hand")
.build())
.transformation(new TransformationComponent(0, 180, 0))
.build();
String condition = String.format("query.block_property('%s') == 0 && query.block_property('%s') == 0", BITS_A_PROPERTY, BITS_B_PROPERTY);
permutations.add(new CustomBlockPermutation(components, condition));
}
private void addFloorPermutations(List<CustomBlockPermutation> permutations) {
String[] quadrantNames = {"a", "b", "c", "d"};
for (int quadrant = 0; quadrant < 4; quadrant++) {
for (int i = 0; i < 4; i++) {
int floorRotation = 4 * quadrant + i;
CustomBlockComponents components = new GeyserCustomBlockComponents.CustomBlockComponentsBuilder()
.selectionBox(FLOOR_BOX)
.collisionBox(FLOOR_BOX)
.geometry(new GeometryComponentBuilder()
.identifier("geometry.geyser.player_skull_floor_" + quadrantNames[i])
.build())
.transformation(new TransformationComponent(0, ROTATIONS[quadrant], 0))
.build();
int bitsA = (5 + floorRotation) % 7;
int bitsB = (5 + floorRotation) / 7;
String condition = String.format("query.block_property('%s') == %d && query.block_property('%s') == %d", BITS_A_PROPERTY, bitsA, BITS_B_PROPERTY, bitsB);
permutations.add(new CustomBlockPermutation(components, condition));
}
}
}
private void addWallPermutations(List<CustomBlockPermutation> permutations) {
for (int i = 0; i < 4; i++) {
CustomBlockComponents components = new GeyserCustomBlockComponents.CustomBlockComponentsBuilder()
.selectionBox(WALL_BOX)
.collisionBox(WALL_BOX)
.geometry(new GeometryComponentBuilder()
.identifier("geometry.geyser.player_skull_wall")
.build())
.transformation(new TransformationComponent(0, ROTATIONS[i], 0))
.build();
String condition = String.format("query.block_property('%s') == %d && query.block_property('%s') == %d", BITS_A_PROPERTY, i + 1, BITS_B_PROPERTY, 0);
permutations.add(new CustomBlockPermutation(components, condition));
}
}
}

View File

@ -30,6 +30,7 @@ import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.With;
@ -39,6 +40,7 @@ import lombok.With;
@ToString
@EqualsAndHashCode
@Getter
@Setter
@With
@NoArgsConstructor
@AllArgsConstructor

View File

@ -27,6 +27,7 @@ package org.geysermc.geyser.registry.type;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import lombok.Builder;
import lombok.Value;
import org.checkerframework.checker.nullness.qual.NonNull;
@ -36,6 +37,7 @@ import org.cloudburstmc.protocol.bedrock.data.inventory.ComponentItemData;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.cloudburstmc.protocol.common.DefinitionRegistry;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.block.custom.CustomBlockData;
import org.geysermc.geyser.inventory.item.StoredItemMappings;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.item.type.Item;
@ -72,6 +74,8 @@ public class ItemMappings implements DefinitionRegistry<ItemDefinition> {
List<ComponentItemData> componentItemData;
Int2ObjectMap<String> customIdMappings;
Object2ObjectMap<CustomBlockData, ItemDefinition> customBlockItemDefinitions;
/**
* Gets an {@link ItemMapping} from the given {@link ItemStack}.
*

View File

@ -464,6 +464,12 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
@Setter
private long lastInteractionTime;
/**
* Stores when the player started to break a block. Used to allow correct break time for custom blocks.
*/
@Setter
private long blockBreakStartTime;
/**
* Stores whether the player intended to place a bucket.
*/
@ -1563,6 +1569,17 @@ public class GeyserSession implements GeyserConnection, GeyserCommandSource {
startGamePacket.setItemDefinitions(this.itemMappings.getItemDefinitions().values().stream().toList()); // TODO
// startGamePacket.setBlockPalette(this.blockMappings.getBedrockBlockPalette());
// Needed for custom block mappings and custom skulls system
startGamePacket.getBlockProperties().addAll(this.blockMappings.getBlockProperties());
// See https://learn.microsoft.com/en-us/minecraft/creator/documents/experimentalfeaturestoggle for info on each experiment
// data_driven_items (Holiday Creator Features) is needed for blocks and items
startGamePacket.getExperiments().add(new ExperimentData("data_driven_items", true));
// Needed for block properties for states
startGamePacket.getExperiments().add(new ExperimentData("upcoming_creator_features", true));
// Needed for certain molang queries used in blocks and items
startGamePacket.getExperiments().add(new ExperimentData("experimental_molang_features", true));
startGamePacket.setVanillaVersion("*");
startGamePacket.setInventoriesServerAuthoritative(true);
startGamePacket.setServerEngine(""); // Do we want to fill this in?

View File

@ -31,9 +31,17 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import lombok.Data;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.entity.type.player.SkullPlayerEntity;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.type.CustomSkull;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.skin.SkinManager;
import java.io.IOException;
import java.util.*;
public class SkullCache {
@ -71,21 +79,44 @@ public class SkullCache {
this.skullRenderDistanceSquared = distance * distance;
}
public void putSkull(Vector3i position, UUID uuid, String texturesProperty, int blockState) {
public Skull putSkull(Vector3i position, UUID uuid, String texturesProperty, int blockState) {
Skull skull = skulls.computeIfAbsent(position, Skull::new);
skull.uuid = uuid;
skull.texturesProperty = texturesProperty;
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) {
String skinUrl = gameProfileData.skinUrl();
skull.skinHash = skinUrl.substring(skinUrl.lastIndexOf('/') + 1);
} else {
session.getGeyser().getLogger().debug("Player skull with invalid Skin tag: " + position + " Textures: " + texturesProperty);
}
} catch (IOException e) {
session.getGeyser().getLogger().debug("Player skull with invalid Skin tag: " + position + " Textures: " + texturesProperty);
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
e.printStackTrace();
}
}
}
skull.blockState = blockState;
skull.blockDefinition = translateCustomSkull(skull.skinHash, blockState);
if (skull.blockDefinition != null) {
reassignSkullEntity(skull);
return skull;
}
if (skull.entity != null) {
skull.entity.updateSkull(skull);
} else {
if (!cullingEnabled) {
assignSkullEntity(skull);
return;
return skull;
}
if (lastPlayerPosition == null) {
return;
return skull;
}
skull.distanceSquared = position.distanceSquared(lastPlayerPosition.getX(), lastPlayerPosition.getY(), lastPlayerPosition.getZ());
if (skull.distanceSquared < skullRenderDistanceSquared) {
@ -105,24 +136,24 @@ public class SkullCache {
}
}
}
return skull;
}
public void removeSkull(Vector3i position) {
Skull skull = skulls.remove(position);
if (skull != null) {
boolean hadEntity = skull.entity != null;
freeSkullEntity(skull);
if (cullingEnabled) {
inRangeSkulls.remove(skull);
if (hadEntity && inRangeSkulls.size() >= maxVisibleSkulls) {
// Reassign entity to the closest skull without an entity
assignSkullEntity(inRangeSkulls.get(maxVisibleSkulls - 1));
}
}
reassignSkullEntity(skull);
}
}
public Skull updateSkull(Vector3i position, int blockState) {
Skull skull = skulls.get(position);
if (skull != null) {
putSkull(position, skull.uuid, skull.texturesProperty, blockState);
}
return skull;
}
public void updateVisibleSkulls() {
if (cullingEnabled) {
// No need to recheck skull visibility for small movements
@ -133,6 +164,10 @@ public class SkullCache {
inRangeSkulls.clear();
for (Skull skull : skulls.values()) {
if (skull.blockDefinition != null) {
continue;
}
skull.distanceSquared = skull.position.distanceSquared(lastPlayerPosition.getX(), lastPlayerPosition.getY(), lastPlayerPosition.getZ());
if (skull.distanceSquared > skullRenderDistanceSquared) {
freeSkullEntity(skull);
@ -191,6 +226,19 @@ public class SkullCache {
}
}
private void reassignSkullEntity(Skull skull) {
boolean hadEntity = skull.entity != null;
freeSkullEntity(skull);
if (cullingEnabled) {
inRangeSkulls.remove(skull);
if (hadEntity && inRangeSkulls.size() >= maxVisibleSkulls) {
// Reassign entity to the closest skull without an entity
assignSkullEntity(inRangeSkulls.get(maxVisibleSkulls - 1));
}
}
}
public void clear() {
skulls.clear();
inRangeSkulls.clear();
@ -199,12 +247,33 @@ public class SkullCache {
lastPlayerPosition = null;
}
private BlockDefinition translateCustomSkull(String skinHash, int blockState) {
CustomSkull customSkull = BlockRegistries.CUSTOM_SKULLS.get(skinHash);
if (customSkull != null) {
byte floorRotation = BlockStateValues.getSkullRotation(blockState);
CustomBlockState customBlockState;
if (floorRotation == -1) {
// Wall skull
int wallDirection = BlockStateValues.getSkullWallDirections().get(blockState);
customBlockState = customSkull.getWallBlockState(wallDirection);
} else {
customBlockState = customSkull.getFloorBlockState(floorRotation);
}
return session.getBlockMappings().getCustomBlockStateDefinitions().get(customBlockState);
}
return null;
}
@RequiredArgsConstructor
@Data
public static class Skull {
private UUID uuid;
private String texturesProperty;
private String skinHash;
private int blockState;
private BlockDefinition blockDefinition;
private SkullPlayerEntity entity;
private final Vector3i position;

View File

@ -277,7 +277,7 @@ public class SkinManager {
return null;
}
static GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException {
public static GameProfileData loadFromJson(String encodedJson) throws IOException, IllegalArgumentException {
JsonNode skinObject = GeyserImpl.JSON_MAPPER.readTree(new String(Base64.getDecoder().decode(encodedJson), StandardCharsets.UTF_8));
JsonNode textures = skinObject.get("textures");

View File

@ -460,7 +460,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
@ -470,7 +470,7 @@ public class SkinProvider {
private static Cape supplyCape(String capeUrl, CapeProvider provider) {
byte[] cape = EMPTY_CAPE.capeData();
try {
cape = requestImage(capeUrl, provider);
cape = requestImageData(capeUrl, provider);
} catch (Exception ignored) {
} // just ignore I guess
@ -527,7 +527,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
@ -587,6 +587,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

@ -57,30 +57,32 @@ public class DoubleChestInventoryTranslator extends ChestInventoryTranslator {
// See BlockInventoryHolder - same concept there except we're also dealing with a specific block state
if (session.getLastInteractionPlayerPosition().equals(session.getPlayerEntity().getPosition())) {
int javaBlockId = session.getGeyser().getWorldManager().getBlockAt(session, session.getLastInteractionBlockPosition());
String[] javaBlockString = BlockRegistries.JAVA_BLOCKS.getOrDefault(javaBlockId, BlockMapping.AIR).getJavaIdentifier().split("\\[");
if (javaBlockString.length > 1 && (javaBlockString[0].equals("minecraft:chest") || javaBlockString[0].equals("minecraft:trapped_chest"))
&& !javaBlockString[1].contains("type=single")) {
inventory.setHolderPosition(session.getLastInteractionBlockPosition());
((Container) inventory).setUsingRealBlock(true, javaBlockString[0]);
if (!BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get().containsKey(javaBlockId)) {
String[] javaBlockString = BlockRegistries.JAVA_BLOCKS.getOrDefault(javaBlockId, BlockMapping.AIR).getJavaIdentifier().split("\\[");
if (javaBlockString.length > 1 && (javaBlockString[0].equals("minecraft:chest") || javaBlockString[0].equals("minecraft:trapped_chest"))
&& !javaBlockString[1].contains("type=single")) {
inventory.setHolderPosition(session.getLastInteractionBlockPosition());
((Container) inventory).setUsingRealBlock(true, javaBlockString[0]);
NbtMapBuilder tag = NbtMap.builder()
.putString("id", "Chest")
.putInt("x", session.getLastInteractionBlockPosition().getX())
.putInt("y", session.getLastInteractionBlockPosition().getY())
.putInt("z", session.getLastInteractionBlockPosition().getZ())
.putString("CustomName", inventory.getTitle())
.putString("id", "Chest");
NbtMapBuilder tag = NbtMap.builder()
.putString("id", "Chest")
.putInt("x", session.getLastInteractionBlockPosition().getX())
.putInt("y", session.getLastInteractionBlockPosition().getY())
.putInt("z", session.getLastInteractionBlockPosition().getZ())
.putString("CustomName", inventory.getTitle())
.putString("id", "Chest");
DoubleChestValue chestValue = BlockStateValues.getDoubleChestValues().get(javaBlockId);
DoubleChestBlockEntityTranslator.translateChestValue(tag, chestValue,
session.getLastInteractionBlockPosition().getX(), session.getLastInteractionBlockPosition().getZ());
DoubleChestValue chestValue = BlockStateValues.getDoubleChestValues().get(javaBlockId);
DoubleChestBlockEntityTranslator.translateChestValue(tag, chestValue,
session.getLastInteractionBlockPosition().getX(), session.getLastInteractionBlockPosition().getZ());
BlockEntityDataPacket dataPacket = new BlockEntityDataPacket();
dataPacket.setData(tag.build());
dataPacket.setBlockPosition(session.getLastInteractionBlockPosition());
session.sendUpstreamPacket(dataPacket);
BlockEntityDataPacket dataPacket = new BlockEntityDataPacket();
dataPacket.setData(tag.build());
dataPacket.setBlockPosition(session.getLastInteractionBlockPosition());
session.sendUpstreamPacket(dataPacket);
return true;
return true;
}
}
}
@ -90,7 +92,7 @@ public class DoubleChestInventoryTranslator extends ChestInventoryTranslator {
}
Vector3i pairPosition = position.add(Vector3i.UNIT_X);
BlockDefinition definition = session.getBlockMappings().getBedrockBlock(defaultJavaBlockState);
BlockDefinition definition = session.getBlockMappings().getVanillaBedrockBlock(defaultJavaBlockState);
UpdateBlockPacket blockPacket = new UpdateBlockPacket();
blockPacket.setDataLayer(0);

View File

@ -28,9 +28,23 @@ package org.geysermc.geyser.translator.inventory.item;
import com.github.steveice10.mc.protocol.data.game.Identifier;
import com.github.steveice10.mc.protocol.data.game.entity.attribute.ModifierOperation;
import com.github.steveice10.mc.protocol.data.game.entity.metadata.ItemStack;
import com.github.steveice10.opennbt.tag.builtin.*;
import com.github.steveice10.opennbt.tag.builtin.ByteArrayTag;
import com.github.steveice10.opennbt.tag.builtin.ByteTag;
import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.DoubleTag;
import com.github.steveice10.opennbt.tag.builtin.FloatTag;
import com.github.steveice10.opennbt.tag.builtin.IntArrayTag;
import com.github.steveice10.opennbt.tag.builtin.IntTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.LongArrayTag;
import com.github.steveice10.opennbt.tag.builtin.LongTag;
import com.github.steveice10.opennbt.tag.builtin.ShortTag;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
import com.github.steveice10.opennbt.tag.builtin.Tag;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
import org.geysermc.geyser.api.block.custom.CustomBlockData;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.cloudburstmc.nbt.NbtList;
@ -41,12 +55,15 @@ import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.item.type.Item;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.type.CustomSkull;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.registry.type.ItemMappings;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.skin.SkinManager;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.text.ChatColor;
import org.geysermc.geyser.text.MinecraftLocale;
import org.geysermc.geyser.translator.text.MessageTranslator;
@ -143,9 +160,21 @@ public final class ItemTranslator {
ItemData.Builder builder = javaItem.translateToBedrock(itemStack, bedrockItem, session.getItemMappings());
if (bedrockItem.isBlock()) {
builder.blockDefinition(bedrockItem.getBedrockBlockDefinition());
CustomBlockData customBlockData = BlockRegistries.CUSTOM_BLOCK_ITEM_OVERRIDES.getOrDefault(
bedrockItem.getJavaItem().javaIdentifier(), null);
if (customBlockData != null) {
translateCustomBlock(customBlockData, session, builder);
} else {
builder.blockDefinition(bedrockItem.getBedrockBlockDefinition());
}
}
if (bedrockItem.getJavaItem().equals(Items.PLAYER_HEAD)) {
translatePlayerHead(session, nbt, builder);
}
translateCustomItem(nbt, builder, bedrockItem);
if (nbt != null) {
// Translate the canDestroy and canPlaceOn Java NBT
ListTag canDestroy = nbt.get("CanDestroy");
@ -362,10 +391,24 @@ public final class ItemTranslator {
ItemMapping mapping = itemStack.asItem().toBedrockDefinition(itemStack.getNbt(), session.getItemMappings());
ItemDefinition itemDefinition = mapping.getBedrockDefinition();
CustomBlockData customBlockData = BlockRegistries.CUSTOM_BLOCK_ITEM_OVERRIDES.getOrDefault(
mapping.getJavaItem().javaIdentifier(), null);
if (customBlockData != null) {
itemDefinition = session.getItemMappings().getCustomBlockItemDefinitions().get(customBlockData);
}
if (mapping.getJavaItem().equals(Items.PLAYER_HEAD)) {
CustomSkull customSkull = getCustomSkull(session, itemStack.getNbt());
if (customSkull != null) {
itemDefinition = session.getItemMappings().getCustomBlockItemDefinitions().get(customSkull.getCustomBlockData());
}
}
ItemDefinition definition = CustomItemTranslator.getCustomItem(itemStack.getNbt(), mapping);
if (definition == null) {
// No custom item
return mapping.getBedrockDefinition();
return itemDefinition;
} else {
return definition;
}
@ -553,6 +596,46 @@ public final class ItemTranslator {
ItemDefinition definition = CustomItemTranslator.getCustomItem(nbt, mapping);
if (definition != null) {
builder.definition(definition);
builder.blockDefinition(null);
}
}
/**
* Translates a custom block override
*/
private static void translateCustomBlock(CustomBlockData customBlockData, GeyserSession session, ItemData.Builder builder) {
ItemDefinition itemDefinition = session.getItemMappings().getCustomBlockItemDefinitions().get(customBlockData);
BlockDefinition blockDefinition = session.getBlockMappings().getCustomBlockStateDefinitions().get(customBlockData.defaultBlockState());
builder.definition(itemDefinition);
builder.blockDefinition(blockDefinition);
}
private static CustomSkull getCustomSkull(GeyserSession session, CompoundTag nbt) {
if (nbt != null && nbt.contains("SkullOwner")) {
if (!(nbt.get("SkullOwner") instanceof CompoundTag skullOwner)) {
// It's a username give up d:
return null;
}
SkinManager.GameProfileData data = SkinManager.GameProfileData.from(skullOwner);
if (data == null) {
session.getGeyser().getLogger().debug("Not sure how to handle skull head item display. " + nbt);
return null;
}
String skinHash = data.skinUrl().substring(data.skinUrl().lastIndexOf('/') + 1);
return BlockRegistries.CUSTOM_SKULLS.get(skinHash);
}
return null;
}
private static void translatePlayerHead(GeyserSession session, CompoundTag nbt, ItemData.Builder builder) {
CustomSkull customSkull = getCustomSkull(session, nbt);
if (customSkull != null) {
CustomBlockData customBlockData = customSkull.getCustomBlockData();
ItemDefinition itemDefinition = session.getItemMappings().getCustomBlockItemDefinitions().get(customBlockData);
BlockDefinition blockDefinition = session.getBlockMappings().getCustomBlockStateDefinitions().get(customBlockData.defaultBlockState());
builder.definition(itemDefinition);
builder.blockDefinition(blockDefinition);
}
}

View File

@ -42,7 +42,7 @@ import org.geysermc.geyser.level.physics.Axis;
import org.geysermc.geyser.level.physics.BoundingBox;
import org.geysermc.geyser.level.physics.CollisionManager;
import org.geysermc.geyser.level.physics.Direction;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.PistonCache;
import org.geysermc.geyser.translator.collision.BlockCollision;
@ -95,7 +95,7 @@ public class PistonBlockEntity {
static {
// Create a ~1 x ~0.5 x ~1 bounding box above the honey block
BlockCollision blockCollision = Registries.COLLISIONS.get(BlockStateValues.JAVA_HONEY_BLOCK_ID);
BlockCollision blockCollision = BlockRegistries.COLLISIONS.get(BlockStateValues.JAVA_HONEY_BLOCK_ID);
if (blockCollision == null) {
throw new RuntimeException("Failed to find honey block collision");
}
@ -485,7 +485,7 @@ public class PistonBlockEntity {
pistonCache.displacePlayer(movement.mul(delta));
} else {
// Move the player out of collision
BlockCollision blockCollision = Registries.COLLISIONS.get(javaId);
BlockCollision blockCollision = BlockRegistries.COLLISIONS.get(javaId);
if (blockCollision != null) {
Vector3d extend = movement.mul(Math.min(1 - blockMovement, 0.5));
Direction movementDirection = orientation;

View File

@ -30,10 +30,14 @@ import com.github.steveice10.opennbt.tag.builtin.CompoundTag;
import com.github.steveice10.opennbt.tag.builtin.IntArrayTag;
import com.github.steveice10.opennbt.tag.builtin.ListTag;
import com.github.steveice10.opennbt.tag.builtin.StringTag;
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket;
import org.geysermc.geyser.GeyserImpl;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.SkullCache;
import org.geysermc.geyser.skin.SkinProvider;
import java.nio.charset.StandardCharsets;
@ -41,6 +45,7 @@ import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@BlockEntity(type = BlockEntityType.SKULL)
public class SkullBlockEntityTranslator extends BlockEntityTranslator implements RequiresBlockState {
@ -91,21 +96,55 @@ public class SkullBlockEntityTranslator extends BlockEntityTranslator implements
return CompletableFuture.completedFuture(texture.getValue());
}
public static void translateSkull(GeyserSession session, CompoundTag tag, int posX, int posY, int posZ, int blockState) {
Vector3i blockPosition = Vector3i.from(posX, posY, posZ);
public static BlockDefinition translateSkull(GeyserSession session, CompoundTag tag, Vector3i blockPosition, int blockState) {
CompoundTag owner = tag.get("SkullOwner");
if (owner == null) {
session.getSkullCache().removeSkull(blockPosition);
return;
return null;
}
UUID uuid = getUUID(owner);
CompletableFuture<String> texturesFuture = getTextures(owner, uuid);
if (texturesFuture.isDone()) {
try {
SkullCache.Skull skull = session.getSkullCache().putSkull(blockPosition, uuid, texturesFuture.get(), blockState);
return skull.getBlockDefinition();
} catch (InterruptedException | ExecutionException e) {
session.getGeyser().getLogger().debug("Failed to acquire textures for custom skull: " + blockPosition + " " + tag);
if (GeyserImpl.getInstance().getConfig().isDebugMode()) {
e.printStackTrace();
}
}
return null;
}
UUID uuid = getUUID(owner);
getTextures(owner, uuid).whenComplete((texturesProperty, throwable) -> {
// SkullOwner contained a username, so we have to wait for it to be retrieved
texturesFuture.whenComplete((texturesProperty, throwable) -> {
if (texturesProperty == null) {
session.getGeyser().getLogger().debug("Custom skull with invalid SkullOwner tag: " + blockPosition + " " + tag);
return;
}
if (session.getEventLoop().inEventLoop()) {
session.getSkullCache().putSkull(blockPosition, uuid, texturesProperty, blockState);
putSkull(session, blockPosition, uuid, texturesProperty, blockState);
} else {
session.executeInEventLoop(() -> session.getSkullCache().putSkull(blockPosition, uuid, texturesProperty, blockState));
session.executeInEventLoop(() -> putSkull(session, blockPosition, uuid, texturesProperty, blockState));
}
});
// We don't have the textures yet, so we can't determine if a custom block was defined for this skull
return null;
}
private static void putSkull(GeyserSession session, Vector3i blockPosition, UUID uuid, String texturesProperty, int blockState) {
SkullCache.Skull skull = session.getSkullCache().putSkull(blockPosition, uuid, texturesProperty, blockState);
if (skull.getBlockDefinition() != null) {
UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
updateBlockPacket.setDataLayer(0);
updateBlockPacket.setBlockPosition(blockPosition);
updateBlockPacket.setDefinition(skull.getBlockDefinition());
updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NEIGHBORS);
updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NETWORK);
session.sendUpstreamPacket(updateBlockPacket);
}
}
}

View File

@ -39,6 +39,7 @@ import org.cloudburstmc.math.vector.Vector3d;
import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType;
import org.cloudburstmc.protocol.bedrock.data.inventory.ItemData;
@ -66,6 +67,7 @@ import org.geysermc.geyser.item.type.SpawnEggItem;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.SkullCache;
import org.geysermc.geyser.skin.FakeHeadProvider;
import org.geysermc.geyser.translator.inventory.InventoryTranslator;
import org.geysermc.geyser.translator.inventory.item.ItemTranslator;
@ -181,6 +183,27 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
}
}
// Check if this is a double placement due to an extended collision block
if (!session.getBlockMappings().getExtendedCollisionBoxes().isEmpty()) {
Vector3i belowBlockPos = null;
switch (packet.getBlockFace()) {
case 1 -> belowBlockPos = blockPos.add(0, -2, 0);
case 2 -> belowBlockPos = blockPos.add(0, -1, 1);
case 3 -> belowBlockPos = blockPos.add(0, -1, -1);
case 4 -> belowBlockPos = blockPos.add(1, -1, 0);
case 5 -> belowBlockPos = blockPos.add(-1, -1, 0);
}
if (belowBlockPos != null) {
int belowBlock = session.getGeyser().getWorldManager().getBlockAt(session, belowBlockPos);
BlockDefinition extendedCollisionDefinition = session.getBlockMappings().getExtendedCollisionBoxes().get(belowBlock);
if (extendedCollisionDefinition != null && (System.currentTimeMillis() - session.getLastInteractionTime()) < 200) {
restoreCorrectBlock(session, blockPos, packet);
return;
}
}
}
// Check to make sure the client isn't spamming interaction
// Based on Nukkit 1.0, with changes to ensure holding down still works
boolean hasAlreadyClicked = System.currentTimeMillis() - session.getLastInteractionTime() < 110.0 &&
@ -265,6 +288,7 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
restoreCorrectBlock(session, blockPos, packet);
return;
}
/*
Block place checks end - client is good to go
*/
@ -524,10 +548,20 @@ public class BedrockInventoryTransactionTranslator extends PacketTranslator<Inve
*/
private void restoreCorrectBlock(GeyserSession session, Vector3i blockPos, InventoryTransactionPacket packet) {
int javaBlockState = session.getGeyser().getWorldManager().getBlockAt(session, blockPos);
BlockDefinition bedrockBlock = session.getBlockMappings().getBedrockBlock(javaBlockState);
if (BlockStateValues.getSkullVariant(javaBlockState) == 3) {
// The changed block was a player skull so check if a custom block was defined for this skull
SkullCache.Skull skull = session.getSkullCache().getSkulls().get(blockPos);
if (skull != null && skull.getBlockDefinition() != null) {
bedrockBlock = skull.getBlockDefinition();
}
}
UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
updateBlockPacket.setDataLayer(0);
updateBlockPacket.setBlockPosition(blockPos);
updateBlockPacket.setDefinition(session.getBlockMappings().getBedrockBlock(javaBlockState));
updateBlockPacket.setDefinition(bedrockBlock);
updateBlockPacket.getFlags().addAll(UpdateBlockPacket.FLAG_ALL_PRIORITY);
session.sendUpstreamPacket(updateBlockPacket);

View File

@ -26,6 +26,10 @@
package org.geysermc.geyser.translator.protocol.bedrock.entity.player;
import com.github.steveice10.mc.protocol.data.game.entity.object.Direction;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundInteractPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerAbilitiesPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerActionPacket;
import com.github.steveice10.mc.protocol.packet.ingame.serverbound.player.ServerboundPlayerCommandPacket;
import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.entity.player.Hand;
import com.github.steveice10.mc.protocol.data.game.entity.player.InteractAction;
@ -36,16 +40,22 @@ import org.cloudburstmc.math.vector.Vector3f;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.data.LevelEvent;
import org.cloudburstmc.protocol.bedrock.data.PlayerActionType;
import org.cloudburstmc.protocol.bedrock.data.definitions.ItemDefinition;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityEventType;
import org.cloudburstmc.protocol.bedrock.data.entity.EntityFlag;
import org.cloudburstmc.protocol.bedrock.packet.*;
import org.geysermc.geyser.api.block.custom.CustomBlockState;
import org.geysermc.geyser.entity.type.Entity;
import org.geysermc.geyser.entity.type.ItemFrameEntity;
import org.geysermc.geyser.entity.type.player.SessionPlayerEntity;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.type.BlockMapping;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.SkullCache;
import org.geysermc.geyser.translator.inventory.item.CustomItemTranslator;
import org.geysermc.geyser.translator.protocol.PacketTranslator;
import org.geysermc.geyser.translator.protocol.Translator;
import org.geysermc.geyser.util.BlockUtils;
@ -145,17 +155,29 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
session.sendDownstreamPacket(stopSleepingPacket);
break;
case START_BREAK: {
// Start the block breaking animation
// Ignore START_BREAK when the player is CREATIVE to avoid Spigot receiving 2 packets it interpets as block breaking. https://github.com/GeyserMC/Geyser/issues/4021
if (session.getGameMode() == GameMode.CREATIVE) {
break;
}
// Start the block breaking animation
int blockState = session.getGeyser().getWorldManager().getBlockAt(session, vector);
LevelEventPacket startBreak = new LevelEventPacket();
startBreak.setType(LevelEvent.BLOCK_START_BREAK);
startBreak.setPosition(vector.toFloat());
double breakTime = BlockUtils.getSessionBreakTime(session, BlockRegistries.JAVA_BLOCKS.get(blockState)) * 20;
// If the block is custom or the breaking item is custom, we must keep track of break time ourselves
GeyserItemStack item = session.getPlayerInventory().getItemInHand();
ItemMapping mapping = item.getMapping(session);
ItemDefinition customItem = mapping.isTool() ? CustomItemTranslator.getCustomItem(item.getNbt(), mapping) : null;
CustomBlockState blockStateOverride = BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get(blockState);
SkullCache.Skull skull = session.getSkullCache().getSkulls().get(vector);
session.setBlockBreakStartTime(0);
if (blockStateOverride != null || customItem != null || (skull != null && skull.getBlockDefinition() != null)) {
session.setBlockBreakStartTime(System.currentTimeMillis());
}
startBreak.setData((int) (65535 / breakTime));
session.setBreakingBlock(blockState);
session.sendUpstreamPacket(startBreak);
@ -196,6 +218,30 @@ public class BedrockActionTranslator extends PacketTranslator<PlayerActionPacket
updateBreak.setType(LevelEvent.BLOCK_UPDATE_BREAK);
updateBreak.setPosition(vectorFloat);
double breakTime = BlockUtils.getSessionBreakTime(session, BlockRegistries.JAVA_BLOCKS.get(breakingBlock)) * 20;
// If the block is custom, we must keep track of when it should break ourselves
long blockBreakStartTime = session.getBlockBreakStartTime();
if (blockBreakStartTime != 0) {
long timeSinceStart = System.currentTimeMillis() - blockBreakStartTime;
// We need to add a slight delay to the break time, otherwise the client breaks blocks too fast
if (timeSinceStart >= (breakTime+=2) * 50) {
// Play break sound and particle
LevelEventPacket effectPacket = new LevelEventPacket();
effectPacket.setPosition(vectorFloat);
effectPacket.setType(LevelEvent.PARTICLE_DESTROY_BLOCK);
effectPacket.setData(session.getBlockMappings().getBedrockBlockId(breakingBlock));
session.sendUpstreamPacket(effectPacket);
// Break the block
ServerboundPlayerActionPacket finishBreakingPacket = new ServerboundPlayerActionPacket(PlayerAction.FINISH_DIGGING,
vector, Direction.VALUES[packet.getFace()], session.getWorldCache().nextPredictionSequence());
session.sendDownstreamPacket(finishBreakingPacket);
session.setBlockBreakStartTime(0);
break;
}
}
updateBreak.setData((int) (65535 / breakTime));
session.sendUpstreamPacket(updateBreak);
break;

View File

@ -110,7 +110,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
case CRAFTING_SHAPELESS -> {
ShapelessRecipeData shapelessRecipeData = (ShapelessRecipeData) recipe.getData();
ItemData output = ItemTranslator.translateToBedrock(session, shapelessRecipeData.getResult());
if (output.equals(ItemData.AIR)) {
if (!output.isValid()) {
// Likely modded item that Bedrock will complain about if it persists
continue;
}
@ -131,7 +131,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
case CRAFTING_SHAPED -> {
ShapedRecipeData shapedRecipeData = (ShapedRecipeData) recipe.getData();
ItemData output = ItemTranslator.translateToBedrock(session, shapedRecipeData.getResult());
if (output.equals(ItemData.AIR)) {
if (!output.isValid()) {
// Likely modded item that Bedrock will complain about if it persists
continue;
}
@ -213,7 +213,7 @@ public class JavaUpdateRecipesTranslator extends PacketTranslator<ClientboundUpd
ItemDescriptorWithCount descriptor = ItemDescriptorWithCount.fromItem(input);
ItemStack javaOutput = stoneCuttingData.getResult();
ItemData output = ItemTranslator.translateToBedrock(session, javaOutput);
if (input.equals(ItemData.AIR) || output.equals(ItemData.AIR)) {
if (!input.isValid() || !output.isValid()) {
// Probably modded items
continue;
}

View File

@ -29,8 +29,10 @@ import com.github.steveice10.mc.protocol.data.game.entity.player.GameMode;
import com.github.steveice10.mc.protocol.data.game.level.block.BlockEntityType;
import com.github.steveice10.mc.protocol.packet.ingame.clientbound.level.ClientboundBlockEntityDataPacket;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
import org.cloudburstmc.protocol.bedrock.data.inventory.ContainerType;
import org.cloudburstmc.protocol.bedrock.packet.ContainerOpenPacket;
import org.cloudburstmc.protocol.bedrock.packet.UpdateBlockPacket;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.translator.level.block.entity.BlockEntityTranslator;
@ -63,8 +65,23 @@ public class JavaBlockEntityDataTranslator extends PacketTranslator<ClientboundB
BlockEntityUtils.updateBlockEntity(session, translator.getBlockEntityTag(type, position.getX(), position.getY(), position.getZ(),
packet.getNbt(), blockState), packet.getPosition());
// Check for custom skulls.
boolean hasCustomHeadBlock = false;
if (session.getPreferencesCache().showCustomSkulls() && packet.getNbt() != null && packet.getNbt().contains("SkullOwner")) {
SkullBlockEntityTranslator.translateSkull(session, packet.getNbt(), position.getX(), position.getY(), position.getZ(), blockState);
BlockDefinition blockDefinition = SkullBlockEntityTranslator.translateSkull(session, packet.getNbt(), position, blockState);
if (blockDefinition != null) {
hasCustomHeadBlock = true;
UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
updateBlockPacket.setDataLayer(0);
updateBlockPacket.setBlockPosition(position);
updateBlockPacket.setDefinition(blockDefinition);
updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NEIGHBORS);
updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NETWORK);
session.sendUpstreamPacket(updateBlockPacket);
}
}
if (!hasCustomHeadBlock) {
BlockEntityUtils.updateBlockEntity(session, translator.getBlockEntityTag(type, position.getX(), position.getY(), position.getZ(),
packet.getNbt(), blockState), packet.getPosition());
}
// If block entity is command block, OP permission level is appropriate, player is in creative mode and the NBT is not empty

View File

@ -40,14 +40,18 @@ import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntImmutableList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.ints.IntLists;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import org.cloudburstmc.math.vector.Vector3i;
import org.cloudburstmc.nbt.NBTOutputStream;
import org.cloudburstmc.nbt.NbtMap;
import org.cloudburstmc.nbt.NbtMapBuilder;
import org.cloudburstmc.nbt.NbtUtils;
import org.cloudburstmc.protocol.bedrock.data.definitions.BlockDefinition;
import org.cloudburstmc.protocol.bedrock.packet.LevelChunkPacket;
import org.geysermc.erosion.util.LecternUtils;
import org.geysermc.geyser.entity.type.ItemFrameEntity;
@ -79,9 +83,12 @@ import static org.geysermc.geyser.util.ChunkUtils.indexYZXtoXZY;
@Translator(packet = ClientboundLevelChunkWithLightPacket.class)
public class JavaLevelChunkWithLightTranslator extends PacketTranslator<ClientboundLevelChunkWithLightPacket> {
private static final ThreadLocal<ExtendedCollisionsStorage> EXTENDED_COLLISIONS_STORAGE = ThreadLocal.withInitial(ExtendedCollisionsStorage::new);
@Override
public void translate(GeyserSession session, ClientboundLevelChunkWithLightPacket packet) {
final boolean useExtendedCollisions = !session.getBlockMappings().getExtendedCollisionBoxes().isEmpty();
if (session.isSpawned()) {
ChunkUtils.updateChunkPosition(session, session.getPlayerEntity().getPosition().toInt());
}
@ -111,19 +118,47 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
try {
ByteBuf in = Unpooled.wrappedBuffer(packet.getChunkData());
boolean extendedCollisionNextSection = false;
for (int sectionY = 0; sectionY < chunkSize; sectionY++) {
ChunkSection javaSection = session.getDownstream().getCodecHelper().readChunkSection(in, biomeGlobalPalette);
javaChunks[sectionY] = javaSection.getChunkData();
javaBiomes[sectionY] = javaSection.getBiomeData();
boolean extendedCollision = extendedCollisionNextSection;
boolean thisExtendedCollisionNextSection = false;
int bedrockSectionY = sectionY + (yOffset - (bedrockDimension.minY() >> 4));
if (bedrockSectionY < 0 || maxBedrockSectionY < bedrockSectionY) {
// Ignore this chunk section since it goes outside the bounds accepted by the Bedrock client
if (useExtendedCollisions) {
EXTENDED_COLLISIONS_STORAGE.get().clear();
}
extendedCollisionNextSection = false;
continue;
}
// No need to encode an empty section...
if (javaSection.isBlockCountEmpty()) {
// Unless we need to send extended collisions
if (useExtendedCollisions) {
if (extendedCollision) {
int blocks = EXTENDED_COLLISIONS_STORAGE.get().bottomLayerCollisions() + 1;
BitArray bedrockData = BitArrayVersion.forBitsCeil(Integer.SIZE - Integer.numberOfLeadingZeros(blocks)).createArray(BlockStorage.SIZE);
BlockStorage layer0 = new BlockStorage(bedrockData, new IntArrayList(blocks));
layer0.idFor(session.getBlockMappings().getBedrockAir().getRuntimeId());
for (int yzx = 0; yzx < BlockStorage.SIZE / 16; yzx++) {
if (EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY) != 0) {
bedrockData.set(indexYZXtoXZY(yzx), layer0.idFor(EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY)));
EXTENDED_COLLISIONS_STORAGE.get().set(yzx, 0, sectionY);
}
}
BlockStorage[] layers = new BlockStorage[]{ layer0 };
sections[bedrockSectionY] = new GeyserChunkSection(layers, bedrockSectionY);
}
EXTENDED_COLLISIONS_STORAGE.get().clear();
extendedCollisionNextSection = false;
}
continue;
}
@ -143,6 +178,24 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
section.getBlockStorageArray()[1].setFullBlock(xzy, session.getBlockMappings().getBedrockWater().getRuntimeId());
}
// Extended collision blocks
if (useExtendedCollisions) {
if (EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY) != 0) {
if (javaId == BlockStateValues.JAVA_AIR_ID) {
section.getBlockStorageArray()[0].setFullBlock(xzy, EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY));
}
EXTENDED_COLLISIONS_STORAGE.get().set(yzx, 0, sectionY);
continue;
}
BlockDefinition aboveBedrockExtendedCollisionDefinition = session.getBlockMappings().getExtendedCollisionBoxes().get(javaId);
if (aboveBedrockExtendedCollisionDefinition != null) {
EXTENDED_COLLISIONS_STORAGE.get().set((yzx + 0x100) & 0xFFF, aboveBedrockExtendedCollisionDefinition.getRuntimeId(), sectionY);
if ((xzy & 0xF) == 15) {
thisExtendedCollisionNextSection = true;
}
}
}
// Check if block is piston or flower to see if we'll need to create additional block entities, as they're only block entities in Bedrock
if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isNonWaterCauldron(javaId)) {
bedrockBlockEntities.add(BedrockOnlyBlockEntity.getTag(session,
@ -152,6 +205,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
}
}
sections[bedrockSectionY] = section;
extendedCollisionNextSection = thisExtendedCollisionNextSection;
continue;
}
@ -167,15 +221,21 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
} else {
sections[bedrockSectionY] = new GeyserChunkSection(new BlockStorage[] {blockStorage}, bedrockSectionY);
}
if (useExtendedCollisions) {
EXTENDED_COLLISIONS_STORAGE.get().clear();
extendedCollisionNextSection = false;
}
// If a chunk contains all of the same piston or flower pot then god help us
continue;
}
IntList bedrockPalette = new IntArrayList(javaPalette.size());
int airPaletteId = -1;
waterloggedPaletteIds.clear();
bedrockOnlyBlockEntityIds.clear();
// Iterate through palette and convert state IDs to Bedrock, doing some additional checks as we go
int extendedCollisionsInPalette = 0;
for (int i = 0; i < javaPalette.size(); i++) {
int javaId = javaPalette.idToState(i);
bedrockPalette.add(session.getBlockMappings().getBedrockBlockId(javaId));
@ -184,6 +244,17 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
waterloggedPaletteIds.set(i);
}
if (javaId == BlockStateValues.JAVA_AIR_ID) {
airPaletteId = i;
}
if (useExtendedCollisions) {
if (session.getBlockMappings().getExtendedCollisionBoxes().get(javaId) != null) {
extendedCollision = true;
extendedCollisionsInPalette++;
}
}
// Check if block is piston, flower or cauldron to see if we'll need to create additional block entities, as they're only block entities in Bedrock
if (BlockStateValues.getFlowerPotValues().containsKey(javaId) || BlockStateValues.getPistonValues().containsKey(javaId) || BlockStateValues.isNonWaterCauldron(javaId)) {
bedrockOnlyBlockEntityIds.set(i);
@ -205,20 +276,29 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
}
}
BitArray bedrockData = BitArrayVersion.forBitsCeil(javaData.getBitsPerEntry()).createArray(BlockStorage.SIZE);
// We need to ensure we use enough bits to represent extended collision blocks in the chunk section
int sectionCollisionBlocks = 0;
if (useExtendedCollisions) {
int bottomLayerCollisions = extendedCollision ? EXTENDED_COLLISIONS_STORAGE.get().bottomLayerCollisions() : 0;
sectionCollisionBlocks = bottomLayerCollisions + extendedCollisionsInPalette;
}
int bedrockDataBits = Integer.SIZE - Integer.numberOfLeadingZeros(javaPalette.size() + sectionCollisionBlocks);
BitArray bedrockData = BitArrayVersion.forBitsCeil(bedrockDataBits).createArray(BlockStorage.SIZE);
BlockStorage layer0 = new BlockStorage(bedrockData, bedrockPalette);
BlockStorage[] layers;
// Convert data array from YZX to XZY coordinate order
if (waterloggedPaletteIds.isEmpty()) {
if (waterloggedPaletteIds.isEmpty() && !extendedCollision) {
// No blocks are waterlogged, simply convert coordinate order
// This could probably be optimized further...
for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
bedrockData.set(indexYZXtoXZY(yzx), javaData.get(yzx));
int paletteId = javaData.get(yzx);
int xzy = indexYZXtoXZY(yzx);
bedrockData.set(xzy, paletteId);
}
layers = new BlockStorage[]{ layer0 };
} else {
} else if (!waterloggedPaletteIds.isEmpty() && !extendedCollision) {
// The section contains waterlogged blocks, we need to convert coordinate order AND generate a V1 block storage for
// layer 1 with palette ID 1 indicating water
int[] layer1Data = new int[BlockStorage.SIZE >> 5];
@ -231,6 +311,64 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
layer1Data[xzy >> 5] |= 1 << (xzy & 0x1F);
}
}
// V1 palette
IntList layer1Palette = IntList.of(
session.getBlockMappings().getBedrockAir().getRuntimeId(), // Air - see BlockStorage's constructor for more information
session.getBlockMappings().getBedrockWater().getRuntimeId());
layers = new BlockStorage[]{ layer0, new BlockStorage(BitArrayVersion.V1.createArray(BlockStorage.SIZE, layer1Data), layer1Palette) };
} else if (waterloggedPaletteIds.isEmpty() && extendedCollision) {
for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
int paletteId = javaData.get(yzx);
int xzy = indexYZXtoXZY(yzx);
bedrockData.set(xzy, paletteId);
if (EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY) != 0) {
if (paletteId == airPaletteId) {
bedrockData.set(xzy, layer0.idFor(EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY)));
}
EXTENDED_COLLISIONS_STORAGE.get().set(yzx, 0, sectionY);
continue;
}
BlockDefinition aboveBedrockExtendedCollisionDefinition = session.getBlockMappings()
.getExtendedCollisionBoxes().get(javaPalette.idToState(paletteId));
if (aboveBedrockExtendedCollisionDefinition != null) {
EXTENDED_COLLISIONS_STORAGE.get().set((yzx + 0x100) & 0xFFF, aboveBedrockExtendedCollisionDefinition.getRuntimeId(), sectionY);
if ((xzy & 0xF) == 15) {
thisExtendedCollisionNextSection = true;
}
}
}
layers = new BlockStorage[]{ layer0 };
} else {
int[] layer1Data = new int[BlockStorage.SIZE >> 5];
for (int yzx = 0; yzx < BlockStorage.SIZE; yzx++) {
int paletteId = javaData.get(yzx);
int xzy = indexYZXtoXZY(yzx);
bedrockData.set(xzy, paletteId);
if (waterloggedPaletteIds.get(paletteId)) {
layer1Data[xzy >> 5] |= 1 << (xzy & 0x1F);
}
if (EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY) != 0) {
if (paletteId == airPaletteId) {
bedrockData.set(xzy, layer0.idFor(EXTENDED_COLLISIONS_STORAGE.get().get(yzx, sectionY)));
}
EXTENDED_COLLISIONS_STORAGE.get().set(yzx, 0, sectionY);
continue;
}
BlockDefinition aboveBedrockExtendedCollisionDefinition = session.getBlockMappings().getExtendedCollisionBoxes()
.get(javaPalette.idToState(paletteId));
if (aboveBedrockExtendedCollisionDefinition != null) {
EXTENDED_COLLISIONS_STORAGE.get().set((yzx + 0x100) & 0xFFF, aboveBedrockExtendedCollisionDefinition.getRuntimeId(), sectionY);
if ((xzy & 0xF) == 15) {
thisExtendedCollisionNextSection = true;
}
}
}
// V1 palette
IntList layer1Palette = IntList.of(
@ -241,6 +379,7 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
}
sections[bedrockSectionY] = new GeyserChunkSection(layers, bedrockSectionY);
extendedCollisionNextSection = thisExtendedCollisionNextSection;
}
if (!session.getErosionHandler().isActive()) {
@ -284,7 +423,21 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
// Check for custom skulls
if (session.getPreferencesCache().showCustomSkulls() && type == BlockEntityType.SKULL && tag != null && tag.contains("SkullOwner")) {
SkullBlockEntityTranslator.translateSkull(session, tag, x + chunkBlockX, y, z + chunkBlockZ, blockState);
BlockDefinition blockDefinition = SkullBlockEntityTranslator.translateSkull(session, tag, Vector3i.from(x + chunkBlockX, y, z + chunkBlockZ), blockState);
if (blockDefinition != null) {
int bedrockSectionY = (y >> 4) - (bedrockDimension.minY() >> 4);
if (0 <= bedrockSectionY && bedrockSectionY < maxBedrockSectionY) {
// Custom skull is in a section accepted by Bedrock
GeyserChunkSection bedrockSection = sections[bedrockSectionY];
IntList palette = bedrockSection.getBlockStorageArray()[0].getPalette();
if (palette instanceof IntImmutableList || palette instanceof IntLists.Singleton) {
// TODO there has to be a better way to expand the palette .-.
bedrockSection = bedrockSection.copy(bedrockSectionY);
sections[bedrockSectionY] = bedrockSection;
}
bedrockSection.setFullBlock(x, y & 0xF, z, 0, blockDefinition.getRuntimeId());
}
}
}
}
@ -380,4 +533,50 @@ public class JavaLevelChunkWithLightTranslator extends PacketTranslator<Clientbo
}
}
}
static final class ExtendedCollisionsStorage {
private int[] data;
private int sectionY;
int get(int index, int sY) {
if (data == null) {
return 0;
}
if (!(sY == sectionY || sY == sectionY + 1)) {
data = null;
return 0;
}
return data[index];
}
void set(int index, int value, int sY) {
ensureDataExists();
data[index] = value;
sectionY = sY;
}
void clear() {
data = null;
}
int bottomLayerCollisions() {
if (data == null) {
return 0;
}
IntSet uniqueNonZeroSet = new IntOpenHashSet();
for (int i = 0; i < BlockStorage.SIZE / 16; i++) {
if (data[i] != 0) {
uniqueNonZeroSet.add(data[i]);
}
}
return uniqueNonZeroSet.size();
}
private void ensureDataExists() {
if (data == null) {
data = new int[BlockStorage.SIZE];
}
}
}
}

View File

@ -30,7 +30,7 @@ import org.cloudburstmc.math.vector.Vector3i;
import org.geysermc.geyser.inventory.GeyserItemStack;
import org.geysermc.geyser.inventory.PlayerInventory;
import org.geysermc.geyser.level.block.BlockStateValues;
import org.geysermc.geyser.registry.Registries;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.registry.type.BlockMapping;
import org.geysermc.geyser.registry.type.ItemMapping;
import org.geysermc.geyser.session.GeyserSession;
@ -219,7 +219,7 @@ public final class BlockUtils {
}
public static BlockCollision getCollision(int blockId) {
return Registries.COLLISIONS.get(blockId);
return BlockRegistries.COLLISIONS.get(blockId);
}
public static BlockCollision getCollisionAt(GeyserSession session, Vector3i blockPos) {

View File

@ -46,6 +46,7 @@ import org.geysermc.geyser.level.chunk.GeyserChunkSection;
import org.geysermc.geyser.level.chunk.bitarray.SingletonBitArray;
import org.geysermc.geyser.registry.BlockRegistries;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.SkullCache;
import org.geysermc.geyser.text.GeyserLocale;
import org.geysermc.geyser.translator.level.block.entity.BedrockOnlyBlockEntity;
@ -129,16 +130,23 @@ public class ChunkUtils {
// Otherwise, let's still store our reference to the item frame, but let the new block take precedence for now
}
if (BlockStateValues.getSkullVariant(blockState) == -1) {
BlockDefinition definition = session.getBlockMappings().getBedrockBlock(blockState);
int skullVariant = BlockStateValues.getSkullVariant(blockState);
if (skullVariant == -1) {
// Skull is gone
session.getSkullCache().removeSkull(position);
} else if (skullVariant == 3) {
// The changed block was a player skull so check if a custom block was defined for this skull
SkullCache.Skull skull = session.getSkullCache().updateSkull(position, blockState);
if (skull != null && skull.getBlockDefinition() != null) {
definition = skull.getBlockDefinition();
}
}
// Prevent moving_piston from being placed
// It's used for extending piston heads, but it isn't needed on Bedrock and causes pistons to flicker
if (!BlockStateValues.isMovingPiston(blockState)) {
BlockDefinition definition = session.getBlockMappings().getBedrockBlock(blockState);
UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
updateBlockPacket.setDataLayer(0);
updateBlockPacket.setBlockPosition(position);
@ -158,6 +166,36 @@ public class ChunkUtils {
session.sendUpstreamPacket(waterPacket);
}
// Extended collision boxes for custom blocks
if (!session.getBlockMappings().getExtendedCollisionBoxes().isEmpty()) {
int aboveBlock = session.getGeyser().getWorldManager().getBlockAt(session, position.getX(), position.getY() + 1, position.getZ());
BlockDefinition aboveBedrockExtendedCollisionDefinition = session.getBlockMappings().getExtendedCollisionBoxes().get(blockState);
int belowBlock = session.getGeyser().getWorldManager().getBlockAt(session, position.getX(), position.getY() - 1, position.getZ());
BlockDefinition belowBedrockExtendedCollisionDefinition = session.getBlockMappings().getExtendedCollisionBoxes().get(belowBlock);
if (belowBedrockExtendedCollisionDefinition != null && blockState == BlockStateValues.JAVA_AIR_ID) {
UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
updateBlockPacket.setDataLayer(0);
updateBlockPacket.setBlockPosition(position);
updateBlockPacket.setDefinition(belowBedrockExtendedCollisionDefinition);
updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NETWORK);
session.sendUpstreamPacket(updateBlockPacket);
} else if (aboveBedrockExtendedCollisionDefinition != null && aboveBlock == BlockStateValues.JAVA_AIR_ID) {
UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
updateBlockPacket.setDataLayer(0);
updateBlockPacket.setBlockPosition(position.add(0, 1, 0));
updateBlockPacket.setDefinition(aboveBedrockExtendedCollisionDefinition);
updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NETWORK);
session.sendUpstreamPacket(updateBlockPacket);
} else if (aboveBlock == BlockStateValues.JAVA_AIR_ID) {
UpdateBlockPacket updateBlockPacket = new UpdateBlockPacket();
updateBlockPacket.setDataLayer(0);
updateBlockPacket.setBlockPosition(position.add(0, 1, 0));
updateBlockPacket.setDefinition(session.getBlockMappings().getBedrockAir());
updateBlockPacket.getFlags().add(UpdateBlockPacket.Flag.NETWORK);
session.sendUpstreamPacket(updateBlockPacket);
}
}
BlockStateValues.getLecternBookStates().handleBlockChange(session, blockState, position);
// Iterates through all Bedrock-only block entity translators and determines if a manual block entity packet

View File

@ -174,6 +174,15 @@ public class FileUtils {
}
}
/**
* @param resource the internal resource to read off from
*
* @return the contents decoded as a UTF-8 String
*/
public static String readToString(String resource) {
return new String(readAllBytes(resource), StandardCharsets.UTF_8);
}
/**
* Read the lines of a file and return it as a stream
*

View File

@ -28,6 +28,12 @@ package org.geysermc.geyser.util;
public class MathUtils {
public static final double SQRT_OF_TWO = Math.sqrt(2);
/**
* Wrap the given float degrees to be between -180.0 and 180.0.
*
* @param degrees The degrees value to wrap
* @return The wrapped degrees value between -180.0 and 180.0
*/
public static float wrapDegrees(float degrees) {
degrees = degrees % 360.0f;
if (degrees < -180.0f) {
@ -38,14 +44,56 @@ public class MathUtils {
return degrees;
}
/**
* Wrap the given double degrees to be between -180.0 and 180.0.
*
* @param degrees The degrees value to wrap
* @return The wrapped degrees value between -180.0 and 180.0
*/
public static float wrapDegrees(double degrees) {
return wrapDegrees((float) degrees);
}
/**
* Wrap the given degrees to be between -180 and 180 as an integer.
*
* @param degrees The degrees value to wrap
* @return The wrapped degrees value between -180 and 180 as an integer
*/
public static int wrapDegreesToInt(float degrees) {
return (int) wrapDegrees(degrees);
}
/**
* Unwrap the given float degrees to be between 0.0 and 360.0.
*
* @param degrees The degrees value to unwrap
* @return The unwrapped degrees value between 0.0 and 360.0
*/
public static float unwrapDegrees(float degrees) {
return (degrees % 360 + 360) % 360;
}
/**
* Unwrap the given double degrees to be between 0.0 and 360.0.
*
* @param degrees The degrees value to unwrap
* @return The unwrapped degrees value between 0.0 and 360.0
*/
public static float unwrapDegrees(double degrees) {
return unwrapDegrees((float) degrees);
}
/**
* Unwrap the given degrees to be between 0 and 360 as an integer.
*
* @param degrees The degrees value to unwrap
* @return The unwrapped degrees value between 0 and 360 as an integer
*/
public static int unwrapDegreesToInt(float degrees) {
return (int) unwrapDegrees(degrees);
}
/**
* Round the given float to the next whole number
*

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,73 @@
{
"format_version": "1.16.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.geyser.player_skull",
"texture_width": 48,
"texture_height": 16
},
"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": {
"north": {"uv": [8, 0], "uv_size": [8, 8]},
"east": {"uv": [0, 0], "uv_size": [8, 8]},
"south": {"uv": [24, 0], "uv_size": [8, 8]},
"west": {"uv": [16, 0], "uv_size": [8, 8]},
"up": {"uv": [32, 0], "uv_size": [8, 8]},
"down": {"uv": [40, 8], "uv_size": [8, -8]}
}
}
]
},
{
"name": "player_skull_hat",
"parent": "player_skull",
"pivot": [0, 24, 0],
"cubes": [
{
"origin": [-4, 8, -4],
"size": [8, 8, 8],
"inflate": 0.25,
"uv": {
"north": {"uv": [8, 8], "uv_size": [8, 8]},
"east": {"uv": [0, 8], "uv_size": [8, 8]},
"south": {"uv": [24, 8], "uv_size": [8, 8]},
"west": {"uv": [16, 8], "uv_size": [8, 8]},
"up": {"uv": [32, 8], "uv_size": [8, 8]},
"down": {"uv": [40, 16], "uv_size": [8, -8]}
}
}
]
}
]
}
]
}

View File

@ -0,0 +1,53 @@
{
"format_version": "1.12.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.geyser.player_skull_floor_${quadrant}",
"texture_width": 48,
"texture_height": 16
},
"bones": [
{
"name": "head",
"pivot": [0, 24, 0],
"rotation": [0, ${y_rotation}, 0],
"cubes": [
{
"origin": [-4, 0.5, -4],
"size": [8, 8, 8],
"uv": {
"north": {"uv": [8, 0], "uv_size": [8, 8]},
"east": {"uv": [0, 0], "uv_size": [8, 8]},
"south": {"uv": [24, 0], "uv_size": [8, 8]},
"west": {"uv": [16, 0], "uv_size": [8, 8]},
"up": {"uv": [32, 0], "uv_size": [8, 8]},
"down": {"uv": [40, 8], "uv_size": [8, -8]}
}
}
]
},
{
"name": "hat",
"parent": "head",
"pivot": [0, 24, 0],
"cubes": [
{
"origin": [-4, 0.5, -4],
"size": [8, 8, 8],
"inflate": 0.5,
"uv": {
"north": {"uv": [8, 8], "uv_size": [8, 8]},
"east": {"uv": [0, 8], "uv_size": [8, 8]},
"south": {"uv": [24, 8], "uv_size": [8, 8]},
"west": {"uv": [16, 8], "uv_size": [8, 8]},
"up": {"uv": [32, 8], "uv_size": [8, 8]},
"down": {"uv": [40, 16], "uv_size": [8, -8]}
}
}
]
}
]
}
]
}

View File

@ -0,0 +1,52 @@
{
"format_version": "1.12.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.geyser.player_skull_hand",
"texture_width": 48,
"texture_height": 16
},
"bones": [
{
"name": "head",
"pivot": [0, 24, 0],
"cubes": [
{
"origin": [-4, 4, -4],
"size": [8, 8, 8],
"uv": {
"north": {"uv": [8, 0], "uv_size": [8, 8]},
"east": {"uv": [0, 0], "uv_size": [8, 8]},
"south": {"uv": [24, 0], "uv_size": [8, 8]},
"west": {"uv": [16, 0], "uv_size": [8, 8]},
"up": {"uv": [32, 0], "uv_size": [8, 8]},
"down": {"uv": [40, 8], "uv_size": [8, -8]}
}
}
]
},
{
"name": "hat",
"parent": "head",
"pivot": [0, 24, 0],
"cubes": [
{
"origin": [-4, 4, -4],
"size": [8, 8, 8],
"inflate": 0.5,
"uv": {
"north": {"uv": [8, 8], "uv_size": [8, 8]},
"east": {"uv": [0, 8], "uv_size": [8, 8]},
"south": {"uv": [24, 8], "uv_size": [8, 8]},
"west": {"uv": [16, 8], "uv_size": [8, 8]},
"up": {"uv": [32, 8], "uv_size": [8, 8]},
"down": {"uv": [40, 16], "uv_size": [8, -8]}
}
}
]
}
]
}
]
}

View File

@ -0,0 +1,52 @@
{
"format_version": "1.12.0",
"minecraft:geometry": [
{
"description": {
"identifier": "geometry.geyser.player_skull_wall",
"texture_width": 48,
"texture_height": 16
},
"bones": [
{
"name": "head",
"pivot": [0, 24, 0],
"cubes": [
{
"origin": [-4, 4, -0.5],
"size": [8, 8, 8],
"uv": {
"north": {"uv": [8, 0], "uv_size": [8, 8]},
"east": {"uv": [0, 0], "uv_size": [8, 8]},
"south": {"uv": [24, 0], "uv_size": [8, 8]},
"west": {"uv": [16, 0], "uv_size": [8, 8]},
"up": {"uv": [32, 0], "uv_size": [8, 8]},
"down": {"uv": [40, 8], "uv_size": [8, -8]}
}
}
]
},
{
"name": "hat",
"parent": "head",
"pivot": [0, 24, 0],
"cubes": [
{
"origin": [-4, 4, -0.5],
"size": [8, 8, 8],
"inflate": 0.5,
"uv": {
"north": {"uv": [8, 8], "uv_size": [8, 8]},
"east": {"uv": [0, 8], "uv_size": [8, 8]},
"south": {"uv": [24, 8], "uv_size": [8, 8]},
"west": {"uv": [16, 8], "uv_size": [8, 8]},
"up": {"uv": [32, 8], "uv_size": [8, 8]},
"down": {"uv": [40, 16], "uv_size": [8, -8]}
}
}
]
}
]
}
]
}

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,7 @@
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_hand.geo.json
skull_resource_pack/models/blocks/player_skull_wall.geo.json
skull_resource_pack/textures/terrain_texture.json
skull_resource_pack/manifest.json

View File

@ -154,9 +154,10 @@ max-visible-custom-skulls: 128
# The radius in blocks around the player in which custom skulls are displayed.
custom-skull-render-distance: 32
# Whether to add (at this time, only) the furnace minecart as a separate item in the game, which normally does not exist in Bedrock Edition.
# Whether to add any items and blocks which normally does not exist in Bedrock Edition.
# This should only need to be disabled if using a proxy that does not use the "transfer packet" style of server switching.
# If this is disabled, furnace minecart items will be mapped to hopper minecart items.
# Geyser's block, item, and skull mappings systems will also be disabled.
# This option requires a restart of Geyser in order to change its setting.
add-non-bedrock-items: true

View File

@ -0,0 +1,29 @@
# --------------------------------
# Geyser Custom Skull Configuration Files
#
# This file is ignored with `add-custom-skull-blocks` disabled.
# See `config.yml` for the main set of configuration values
#
# Custom skulls with the player username, UUID, or texture specified in this file
# will be translated as custom blocks and be displayed in the inventory and on entities.
# --------------------------------
# Java player usernames
# Skins will be updated when Geyser starts and players will have to re-download
# the resource pack if any players had changed their skin.
player-usernames:
# - GeyserMC
# Java player UUIDs
# Skins will be updated when Geyser starts and players will have to re-download
# the resource pack if any players had changed their skin.
player-uuids:
# - 8b8d8e8f-2759-47c6-acb5-5827de8a72b8
# The long string of characters found in the NBT of custom player heads
player-profiles:
# - ewogICJ0aW1lc3RhbXAiIDogMTY1NzMyMjIzOTgzMywKICAicHJvZmlsZUlkIiA6ICJjZGRiZTUyMGQwNDM0YThiYTFjYzlmYzkyZmRlMmJjZiIsCiAgInByb2ZpbGVOYW1lIiA6ICJBbWJlcmljaHUiLAogICJ0ZXh0dXJlcyIgOiB7CiAgICAiU0tJTiIgOiB7CiAgICAgICJ1cmwiIDogImh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTkwNzkwYzU3ZTE4MWVkMTNhZGVkMTRjNDdlZTJmN2M4ZGUzNTMzZTAxN2JhOTU3YWY3YmRmOWRmMWJkZTk0ZiIsCiAgICAgICJtZXRhZGF0YSIgOiB7CiAgICAgICAgIm1vZGVsIiA6ICJzbGltIgogICAgICB9CiAgICB9CiAgfQp9
# The hash of the skin on Minecraft's skin server (http://textures.minecraft.net/texture/HASH)
skin-hashes:
# - a90790c57e181ed13aded14c47ee2f7c8de3533e017ba957af7bdf9df1bde94f

View File

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 113 KiB

View File

@ -6,4 +6,6 @@ org.gradle.parallel=true
org.gradle.vfs.watch=false
group=org.geysermc
version=2.1.1-SNAPSHOT
id=geyser
version=2.2.0-SNAPSHOT
description=Allows for players from Minecraft: Bedrock Edition to join Minecraft: Java Edition servers.