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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@

package com.viaversion.viafabricplus.bedrock.friends;

import com.viaversion.viafabricplus.bedrock.render.BedrockPlayerSkins;
import com.viaversion.viaversion.api.connection.UserConnection;
import java.util.Map;
import java.util.UUID;
import net.raphimc.viabedrock.protocol.model.SkinData;
import net.raphimc.viabedrock.protocol.provider.SkinProvider;

/** Adds the host-issued session nonce to Bedrock's client data for friend worlds. */
Expand All @@ -38,4 +41,9 @@ public Map<String, Object> getClientPlayerSkin(final UserConnection user) {
return claims;
}

@Override
public void setSkin(final UserConnection user, final UUID playerUuid, final SkinData skin) {
BedrockPlayerSkins.accept(user, playerUuid, skin);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock
* Copyright (C) 2026 ViaVersion and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.viaversion.viafabricplus.bedrock.injection.mixin.features.render;

import com.viaversion.viafabricplus.bedrock.render.BedrockPlayerSkins;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.world.entity.player.PlayerSkin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

@Mixin(AbstractClientPlayer.class)
public abstract class MixinAbstractClientPlayer {

@Inject(method = "getSkin", at = @At("RETURN"), cancellable = true)
private void bedrockSkin(final CallbackInfoReturnable<PlayerSkin> cir) {
final AbstractClientPlayer player = (AbstractClientPlayer) (Object) this;
cir.setReturnValue(BedrockPlayerSkins.skin(player.getUUID(), cir.getReturnValue()));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock
* Copyright (C) 2026 ViaVersion and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.viaversion.viafabricplus.bedrock.injection.mixin.features.render;

import com.viaversion.viafabricplus.bedrock.render.CustomEntityRenderStore;
import java.util.List;
import net.raphimc.viabedrock.api.model.entity.CustomEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

@Mixin(value = CustomEntity.class, remap = false)
public abstract class MixinCustomEntity {

@Shadow
private List<CustomEntity.EvaluatedModel> models;

@Shadow
private boolean spawned;

@Inject(method = "evaluateRenderControllerChange", at = @At("RETURN"))
private void publishRenderModels(final CallbackInfoReturnable<Boolean> cir) {
if (cir.getReturnValueZ()) {
CustomEntityRenderStore.update(((CustomEntity) (Object) this).javaUuid(), this.models);
}
}

@Inject(method = "spawn", at = @At("HEAD"), cancellable = true)
private void useNativeRenderer(final CallbackInfo ci) {
this.spawned = true;
ci.cancel();
}

@Inject(method = "remove", at = @At("HEAD"))
private void removeRenderModels(final CallbackInfo ci) {
CustomEntityRenderStore.remove(((CustomEntity) (Object) this).javaUuid());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock
* Copyright (C) 2026 ViaVersion and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.viaversion.viafabricplus.bedrock.injection.mixin.features.render;

import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.viaversion.viafabricplus.bedrock.render.BedrockEntityRenderer;
import com.viaversion.viafabricplus.bedrock.render.BedrockPlayerSkins;
import java.util.HashMap;
import java.util.Map;
import net.minecraft.client.player.AbstractClientPlayer;
import net.minecraft.client.renderer.entity.EntityRenderDispatcher;
import net.minecraft.client.renderer.entity.EntityRenderer;
import net.minecraft.client.renderer.entity.EntityRendererProvider;
import net.minecraft.client.renderer.entity.EntityRenderers;
import net.minecraft.client.renderer.entity.player.AvatarRenderer;
import net.minecraft.client.renderer.entity.state.AvatarRenderState;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.EntityTypes;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

@Mixin(EntityRenderDispatcher.class)
public abstract class MixinEntityRenderDispatcher {

@WrapOperation(method = "onResourceManagerReload", at = @At(value = "INVOKE",
target = "Lnet/minecraft/client/renderer/entity/EntityRenderers;createEntityRenderers(Lnet/minecraft/client/renderer/entity/EntityRendererProvider$Context;)Ljava/util/Map;"))
private Map<EntityType<?>, EntityRenderer<?, ?>> bedrockInteractionRenderer(
final EntityRendererProvider.Context context,
final Operation<Map<EntityType<?>, EntityRenderer<?, ?>>> original
) {
final Map<EntityType<?>, EntityRenderer<?, ?>> renderers = new HashMap<>(original.call(context));
renderers.put(EntityTypes.INTERACTION, new BedrockEntityRenderer(context));
BedrockPlayerSkins.rendererContext(context);
return renderers;
}

@SuppressWarnings("unchecked")
@Inject(method = "getRenderer(Lnet/minecraft/world/entity/Entity;)Lnet/minecraft/client/renderer/entity/EntityRenderer;", at = @At("HEAD"), cancellable = true)
private <T extends Entity> void bedrockPlayerRenderer(final T entity, final CallbackInfoReturnable<EntityRenderer<? super T, ?>> cir) {
if (entity instanceof AbstractClientPlayer player) {
final AvatarRenderer<?> renderer = BedrockPlayerSkins.renderer(player.getUUID());
if (renderer != null) {
cir.setReturnValue((EntityRenderer<? super T, ?>) renderer);
}
}
}

@Inject(method = "getRenderer(Lnet/minecraft/client/renderer/entity/state/AvatarRenderState;)Lnet/minecraft/client/renderer/entity/player/AvatarRenderer;", at = @At("HEAD"), cancellable = true)
private void bedrockPlayerRenderState(final AvatarRenderState state, final CallbackInfoReturnable<AvatarRenderer<?>> cir) {
final AvatarRenderer<?> renderer = BedrockPlayerSkins.renderer(state.skin);
if (renderer != null) {
cir.setReturnValue(renderer);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock
* Copyright (C) 2026 ViaVersion and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.viaversion.viafabricplus.bedrock.injection.mixin.features.render;

import com.viaversion.viafabricplus.bedrock.render.BedrockPlayerSkins;
import com.viaversion.viafabricplus.bedrock.render.CustomEntityRenderStore;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientLevel;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

@Mixin(Minecraft.class)
public abstract class MixinMinecraftSkinLifecycle {

@Inject(method = "setLevel", at = @At("HEAD"))
private void clearBedrockSkinsOnDisconnect(final ClientLevel level, final CallbackInfo ci) {
if (level == null) {
BedrockPlayerSkins.clear();
CustomEntityRenderStore.clear();
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* This file is part of ViaFabricPlus Bedrock - https://github.com/ViaVersionAddons/viafabricplus-bedrock
* Copyright (C) 2026 ViaVersion and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.viaversion.viafabricplus.bedrock.render;

import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Axis;
import com.viaversion.viafabricplus.ViaFabricPlus;
import com.viaversion.viaversion.api.connection.UserConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import net.minecraft.client.model.Model;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.client.renderer.culling.Frustum;
import net.minecraft.client.renderer.entity.EntityRenderer;
import net.minecraft.client.renderer.entity.EntityRendererProvider;
import net.minecraft.client.renderer.entity.state.EntityRenderState;
import net.minecraft.client.renderer.state.level.CameraRenderState;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.resources.Identifier;
import net.minecraft.world.entity.Interaction;
import net.raphimc.viabedrock.api.model.entity.CustomEntity;
import net.raphimc.viabedrock.api.util.StringUtil;
import net.raphimc.viabedrock.protocol.storage.ResourcePackStorage;
import org.cube.converter.model.impl.bedrock.BedrockGeometryModel;

/** Renders ViaBedrock custom actors while leaving ordinary interactions invisible. */
public final class BedrockEntityRenderer extends EntityRenderer<Interaction, BedrockEntityRenderer.State> {

private final Map<String, Model<EntityRenderState>> models = new HashMap<>();
private final Map<List<CustomEntity.EvaluatedModel>, List<RenderedModel>> resolvedModelSets = new HashMap<>();
private ResourcePackStorage currentPacks;

public BedrockEntityRenderer(final EntityRendererProvider.Context context) {
super(context);
}

@Override
public State createRenderState() {
return new State();
}

@Override
public boolean shouldRender(final Interaction entity, final Frustum frustum, final double x, final double y, final double z, final float partialTicks) {
if (CustomEntityRenderStore.get(entity.getUUID()) != null) {
return x * x + y * y + z * z <= 96D * 96D;
}
return super.shouldRender(entity, frustum, x, y, z, partialTicks);
}

@Override
public void extractRenderState(final Interaction entity, final State state, final float partialTicks) {
super.extractRenderState(entity, state, partialTicks);
state.models = List.of();
final UserConnection connection = ViaFabricPlus.api().userConnection();
if (connection == null || !connection.has(ResourcePackStorage.class)) {
this.currentPacks = null;
this.models.clear();
this.resolvedModelSets.clear();
return;
}
final List<CustomEntity.EvaluatedModel> evaluatedModels = CustomEntityRenderStore.get(entity.getUUID());
if (evaluatedModels == null) {
return;
}
final ResourcePackStorage packs = connection.get(ResourcePackStorage.class);
if (this.currentPacks != packs) {
this.currentPacks = packs;
this.models.clear();
this.resolvedModelSets.clear();
}
state.models = this.resolvedModelSets.computeIfAbsent(evaluatedModels, models -> this.resolveModels(models, packs));
state.yaw = entity.getYRot();
}

private List<RenderedModel> resolveModels(
final List<CustomEntity.EvaluatedModel> evaluatedModels,
final ResourcePackStorage packs
) {
final List<RenderedModel> resolved = new ArrayList<>();
for (var evaluated : evaluatedModels) {
final BedrockGeometryModel geometry = packs.getModels().entityModels().get(evaluated.geometryValue());
if (geometry == null) {
continue;
}
final Model<EntityRenderState> model = this.models.computeIfAbsent(evaluated.geometryValue(), _ -> BedrockGeometry.entityModel(geometry));
final String path = "textures/item/entities/" + StringUtil.makeIdentifierValueSafe(evaluated.textureValue().replace("textures/", "")) + ".png";
resolved.add(new RenderedModel(model, Identifier.fromNamespaceAndPath("viabedrock", path)));
}
return List.copyOf(resolved);
}

@Override
public void submit(final State state, final PoseStack poseStack, final SubmitNodeCollector nodes, final CameraRenderState camera) {
for (RenderedModel rendered : state.models) {
poseStack.pushPose();
poseStack.rotateDegrees(Axis.YP, 180F - state.yaw);
poseStack.scale(-1F, -1F, 1F);
poseStack.translate(0F, -1.5F, 0F);
nodes.submitModel(rendered.model, state, poseStack, rendered.model.renderType(rendered.texture), state.lightCoords, OverlayTexture.NO_OVERLAY, -1);
poseStack.popPose();
}
}

public static final class State extends EntityRenderState {
private List<RenderedModel> models = List.of();
private float yaw;
}

private record RenderedModel(Model<EntityRenderState> model, Identifier texture) {
}

}
Loading
Loading