Skip to content

Commit

Permalink
Merge pull request #161 from Ladysnake/feature/c2s-messages
Browse files Browse the repository at this point in the history
Add experimental client-to-server component messaging API
  • Loading branch information
Pyrofab authored Apr 19, 2024
2 parents d6844af + 161e611 commit 51c9c60
Show file tree
Hide file tree
Showing 9 changed files with 270 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,26 @@ public interface ComponentContainer extends NbtSerializable {
@AsmGeneratedCallback(ClientUnloadAwareComponent.class)
void onClientUnload();

/**
* Reverse-lookup for a key based on a component
*
* <p>If a component instance is associated to multiple keys, this will return the first one found.
*
* @param component the component of which to look up the key
* @return the found key, or {@code null} if the component is not part of this container
* @since 5.3.0
*/
@Nullable
@ApiStatus.Experimental
default ComponentKey<?> getKey(Component component) {
for (ComponentKey<?> key : keys()) {
if (key.getInternal(this) == component) {
return key;
}
}
return null;
}

/**
* Reads this object's properties from a {@link NbtCompound}.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Cardinal-Components-API
* Copyright (C) 2019-2024 Ladysnake
*
* 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.
*/
package org.ladysnake.cca.api.v3.component.sync;

import net.minecraft.network.RegistryByteBuf;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;

/**
* @since 6.0.0
*/
@ApiStatus.Experimental
@FunctionalInterface
public interface C2SComponentPacketWriter {
/**
* A no-op writer, for when simply sending an empty message is enough
*/
C2SComponentPacketWriter EMPTY = buf -> {};

@Contract(mutates = "param")
void writeC2SPacket(RegistryByteBuf buf);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Cardinal-Components-API
* Copyright (C) 2019-2024 Ladysnake
*
* 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.
*/
package org.ladysnake.cca.api.v3.entity;

import com.demonwav.mcdev.annotations.CheckEnv;
import com.demonwav.mcdev.annotations.Env;
import com.mojang.datafixers.util.Unit;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
import net.fabricmc.fabric.api.networking.v1.PacketSender;
import net.minecraft.client.MinecraftClient;
import net.minecraft.network.PacketCallbacks;
import net.minecraft.network.RegistryByteBuf;
import org.jetbrains.annotations.ApiStatus;
import org.ladysnake.cca.api.v3.component.Component;
import org.ladysnake.cca.api.v3.component.ComponentKey;
import org.ladysnake.cca.api.v3.component.ComponentProvider;
import org.ladysnake.cca.api.v3.component.sync.C2SComponentPacketWriter;
import org.ladysnake.cca.internal.base.ComponentUpdatePayload;
import org.ladysnake.cca.internal.entity.CardinalComponentsEntity;

import java.util.Objects;

/**
* A component that is attached to players and that can send messages from the client to the serverside version of itself
* @since 6.0.0
*/
@ApiStatus.Experimental
public interface C2SSelfMessagingComponent extends Component {
/**
* Handles a message sent through this component's channel by the player to which this component is attached
*
* <p>Mods should make sure all preconditions are considered when applying changes based on the message - e.g. "is the player an operator" and/or "does the player have the right menu opened".
*
* @param buf the buffer containing the data as written in {@link #sendC2SMessage}
*/
void handleC2SMessage(RegistryByteBuf buf);

/**
* Produces and sends a C2S update packet using the given information
*
* @param writer a {@link C2SComponentPacketWriter} writing the component's data to the packet
* @implNote the default implementation should generally be overridden.
* It finds the component key through a reverse component lookup on the client's main player object,
* which is inefficient and may even produce incorrect results in some edge cases.
* @see #sendC2SMessage(ComponentKey, C2SComponentPacketWriter)
*/
@CheckEnv(Env.CLIENT)
default void sendC2SMessage(C2SComponentPacketWriter writer) {
ComponentProvider provider = (ComponentProvider) Objects.requireNonNull(MinecraftClient.getInstance().player);
ComponentKey<?> key = Objects.requireNonNull(provider.getComponentContainer().getKey(this));
sendC2SMessage(key, writer);
}

/**
* Produces and sends a C2S update packet using the given information
*
* @param key the key describing the component being sent an update
* @param writer a {@link C2SComponentPacketWriter} writing the component's data to the packet
*/
@CheckEnv(Env.CLIENT)
static void sendC2SMessage(ComponentKey<?> key, C2SComponentPacketWriter writer) {
PacketSender sender = ClientPlayNetworking.getSender(); // checks that the player is in game
RegistryByteBuf buf = new RegistryByteBuf(PacketByteBufs.create(), Objects.requireNonNull(MinecraftClient.getInstance().getNetworkHandler()).getRegistryManager());
buf.writeIdentifier(key.getId());
writer.writeC2SPacket(buf);
sender.sendPacket(new ComponentUpdatePayload<>(CardinalComponentsEntity.C2S_SELF_PACKET_ID, Unit.INSTANCE, true, key.getId(), buf), PacketCallbacks.always(buf::release));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,36 @@
*/
package org.ladysnake.cca.internal.entity;

import com.mojang.datafixers.util.Unit;
import net.fabricmc.fabric.api.entity.event.v1.ServerLivingEntityEvents;
import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerEntityEvents;
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.network.codec.PacketCodecs;
import net.minecraft.network.packet.CustomPayload;
import net.minecraft.network.packet.s2c.common.CustomPayloadS2CPacket;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
import net.minecraft.world.GameRules;
import org.ladysnake.cca.api.v3.component.Component;
import org.ladysnake.cca.api.v3.component.ComponentKey;
import org.ladysnake.cca.api.v3.component.ComponentProvider;
import org.ladysnake.cca.api.v3.component.sync.AutoSyncedComponent;
import org.ladysnake.cca.api.v3.entity.C2SSelfMessagingComponent;
import org.ladysnake.cca.api.v3.entity.PlayerSyncCallback;
import org.ladysnake.cca.api.v3.entity.RespawnCopyStrategy;
import org.ladysnake.cca.api.v3.entity.TrackingStartCallback;
import org.ladysnake.cca.internal.base.ComponentUpdatePayload;
import org.ladysnake.cca.internal.base.ComponentsInternals;
import org.ladysnake.cca.internal.base.MorePacketCodecs;

import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

public final class CardinalComponentsEntity {
Expand All @@ -52,12 +62,40 @@ public final class CardinalComponentsEntity {
* called on the game thread.
*/
public static final CustomPayload.Id<ComponentUpdatePayload<Integer>> PACKET_ID = CustomPayload.id("cardinal-components:entity_sync");
/**
* {@link net.minecraft.network.packet.c2s.common.CustomPayloadC2SPacket} channel for C2S player component messages.
*
* <p> Packets emitted on this channel must begin with the {@link ComponentKey#getId() component's type} (as an Identifier).
*
* <p> Components synchronized through this channel will have {@linkplain org.ladysnake.cca.api.v3.entity.C2SSelfMessagingComponent#handleC2SMessage(PacketByteBuf)}
* called on the game thread.
*/
public static final CustomPayload.Id<ComponentUpdatePayload<Unit>> C2S_SELF_PACKET_ID = CustomPayload.id("cardinal-components:player_message_c2s");
private static final Set<Identifier> unknownC2SPlayerComponents = new HashSet<>();

public static void init() {
if (FabricLoader.getInstance().isModLoaded("fabric-networking-api-v1")) {
ComponentUpdatePayload.register(PACKET_ID, PacketCodecs.VAR_INT);
PayloadTypeRegistry.playC2S().register(C2S_SELF_PACKET_ID, ComponentUpdatePayload.codec(C2S_SELF_PACKET_ID, MorePacketCodecs.EMPTY));
PlayerSyncCallback.EVENT.register(player -> syncEntityComponents(player, player));
TrackingStartCallback.EVENT.register(CardinalComponentsEntity::syncEntityComponents);
ServerPlayNetworking.registerGlobalReceiver(CardinalComponentsEntity.C2S_SELF_PACKET_ID, (payload, ctx) -> {
try {
Optional<ComponentKey<?>> componentKey = payload.componentKey();
if (componentKey.isPresent()) {
if (componentKey.get().getNullable(ctx.player()) instanceof C2SSelfMessagingComponent synced) {
synced.handleC2SMessage(payload.buf());
} else if (payload.required() && (unknownC2SPlayerComponents.add(payload.componentKeyId()) || FabricLoader.getInstance().isDevelopmentEnvironment())) {
// In prod, only log the first time an unknown component update is received
ComponentsInternals.LOGGER.warn("[Cardinal Components API] Received an update for component {} from player {}, but this component is not registered for player entities", payload.componentKeyId(), ctx.player());
}
} else if (payload.required() && (unknownC2SPlayerComponents.add(payload.componentKeyId()) || FabricLoader.getInstance().isDevelopmentEnvironment())) {
ComponentsInternals.LOGGER.warn("[Cardinal Components API] Received an update for unknown component {} from player {}", payload.componentKeyId(), ctx.player());
}
} finally {
payload.buf().release();
}
});
}
if (FabricLoader.getInstance().isModLoaded("fabric-lifecycle-events-v1")) {
ServerEntityEvents.ENTITY_LOAD.register((entity, world) -> ((ComponentProvider) entity).getComponentContainer().onServerLoad());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public Iterable<ServerPlayerEntity> getRecipientsForComponentSync() {
return List.of();
}

@SuppressWarnings("AddedMixinMembersNamePattern") // it's okay, we have custom types in the descriptor
@Override
public <C extends AutoSyncedComponent> ComponentUpdatePayload<?> toComponentPacket(ComponentKey<? super C> key, boolean required, RegistryByteBuf data) {
return new ComponentUpdatePayload<>(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Cardinal-Components-API
* Copyright (C) 2019-2024 Ladysnake
*
* 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.
*/
package org.ladysnake.cca.test.entity;

import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents;
import net.minecraft.entity.mob.ShulkerEntity;
import org.ladysnake.cca.api.v3.component.sync.C2SComponentPacketWriter;
import org.ladysnake.cca.test.base.Vita;

public class CcaEntityTestClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
ClientTickEvents.END_CLIENT_TICK.register(client -> {
if (client.targetedEntity instanceof ShulkerEntity) {
((PlayerVita) Vita.get(client.player)).sendC2SMessage(C2SComponentPacketWriter.EMPTY);
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@
import net.minecraft.network.RegistryByteBuf;
import net.minecraft.particle.ParticleTypes;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import org.jetbrains.annotations.NotNull;
import org.ladysnake.cca.api.v3.component.sync.AutoSyncedComponent;
import org.ladysnake.cca.api.v3.component.sync.PlayerSyncPredicate;
import org.ladysnake.cca.api.v3.component.tick.ServerTickingComponent;
import org.ladysnake.cca.api.v3.entity.C2SSelfMessagingComponent;
import org.ladysnake.cca.api.v3.entity.RespawnableComponent;
import org.ladysnake.cca.test.base.BaseVita;
import org.ladysnake.cca.test.base.CardinalGameTest;
Expand All @@ -40,7 +42,7 @@
/**
* A Vita component attached to players, and automatically synchronized with their owner
*/
public class PlayerVita extends EntityVita implements AutoSyncedComponent, ServerTickingComponent, RespawnableComponent<BaseVita> {
public class PlayerVita extends EntityVita implements AutoSyncedComponent, ServerTickingComponent, RespawnableComponent<BaseVita>, C2SSelfMessagingComponent {
public static final int INCREASE_VITA = 0b10;
public static final int DECREASE_VITA = 0b100;

Expand Down Expand Up @@ -108,4 +110,9 @@ public void copyForRespawn(@NotNull BaseVita original, boolean lossless, boolean
this.vitality -= 5;
}
}

@Override
public void handleC2SMessage(RegistryByteBuf buf) {
((PlayerEntity) this.owner).sendMessage(Text.of("Sync!"), true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Cardinal-Components-API
* Copyright (C) 2019-2024 Ladysnake
*
* 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.
*/
@MethodsReturnNonnullByDefault
@ParametersAreNonnullByDefault
package org.ladysnake.cca.test.entity;

import org.ladysnake.cca.api.v3.util.MethodsReturnNonnullByDefault;

import javax.annotation.ParametersAreNonnullByDefault;
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
"main": [
"org.ladysnake.cca.test.entity.CcaEntityTestMod"
],
"client": [
"org.ladysnake.cca.test.entity.CcaEntityTestClient"
],
"cardinal-components": [
"org.ladysnake.cca.test.entity.CcaEntityTestMod"
],
Expand Down

0 comments on commit 51c9c60

Please sign in to comment.