ViaVersion/api/src/main/java/com/viaversion/viaversion/api/protocol/AbstractProtocol.java

478 lines
22 KiB
Java
Raw Normal View History

/*
* This file is part of ViaVersion - https://github.com/ViaVersion/ViaVersion
2024-01-12 10:01:48 +01:00
* Copyright (C) 2016-2024 ViaVersion and contributors
*
* 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 com.viaversion.viaversion.api.protocol;
import com.google.common.base.Preconditions;
import com.viaversion.viaversion.api.Via;
Refactor entity tracking and meta handling This essentially merges the two approaches to the metadata handling from ViaVersion and ViaBackwards and improves on both designs. ViaVersion did not track every single entity, but only those needed (at least in theory) and can work with untracked entities' metadata. It had a very simple method overridden by metadata rewriter implementations, directly operating on the full metadata list and manually handling meta index changes as well as item/block/particle id changes. ViaBackwards on the other hand had to track *every single* entity and threw warnings otherwise - while less prone to errors due to giving obvious warnings in the console, it unnecessarily tracks a lot of entities, and those warnings also annoys users when encountering virtual entity plugins (operating asynchronously and sending update packets while already untracked or not yet tracked). Dedicated MetaHandlers made id changes and filtering a lot easier to read and write. However, the actual metadata list handling and its distribution to handlers was not very well implemented and required a lot of list copying and creation as well as exception throws to cancel individual metadata entries. This version has MetaFilters built with a Builder containing multiple helper functions, and the entity tracking is properly given its own map, hashed by a Protocol's class, to be easily and generically accessible from anywhere with only a Protocol class from the UserConnection, along with more optimized metadata list iteration. The entity tracking is largely unchanged, keeping ViaVersion's approach to not having to track *all* entities (and being able to handle null types in meta handlers). All of this is by no means absolutely perfect, but is much less prone to errors than both previous systems and takes a lot less effort to actually write. A last possible change would be to use a primitive int to object map that is built to be concurrency save for the EntityTracker, tho that would have to be chosen carefully.
2021-05-24 23:24:50 +02:00
import com.viaversion.viaversion.api.connection.UserConnection;
import com.viaversion.viaversion.api.data.entity.EntityTracker;
import com.viaversion.viaversion.api.protocol.packet.ClientboundPacketType;
import com.viaversion.viaversion.api.protocol.packet.Direction;
import com.viaversion.viaversion.api.protocol.packet.PacketType;
import com.viaversion.viaversion.api.protocol.packet.PacketWrapper;
import com.viaversion.viaversion.api.protocol.packet.ServerboundPacketType;
import com.viaversion.viaversion.api.protocol.packet.State;
import com.viaversion.viaversion.api.protocol.packet.mapping.PacketMapping;
import com.viaversion.viaversion.api.protocol.packet.mapping.PacketMappings;
import com.viaversion.viaversion.api.protocol.packet.provider.PacketTypeMap;
import com.viaversion.viaversion.api.protocol.packet.provider.PacketTypesProvider;
import com.viaversion.viaversion.api.protocol.packet.provider.SimplePacketTypesProvider;
import com.viaversion.viaversion.api.protocol.remapper.PacketHandler;
import com.viaversion.viaversion.api.rewriter.MappingDataListener;
2023-02-20 12:02:25 +01:00
import com.viaversion.viaversion.api.rewriter.Rewriter;
import com.viaversion.viaversion.exception.CancelException;
import com.viaversion.viaversion.exception.InformativeException;
import com.viaversion.viaversion.util.ProtocolUtil;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.logging.Level;
import org.checkerframework.checker.nullness.qual.Nullable;
import static com.viaversion.viaversion.util.ProtocolUtil.packetTypeMap;
/**
* Abstract protocol class to handle packet transformation between two protocol versions.
*
2023-02-12 21:14:10 +01:00
* @param <CU> unmapped clientbound packet type
* @param <CM> mapped clientbound packet type
* @param <SM> mapped serverbound packet type
* @param <SU> unmapped serverbound packet type
*/
2023-02-12 21:14:10 +01:00
public abstract class AbstractProtocol<CU extends ClientboundPacketType, CM extends ClientboundPacketType,
SM extends ServerboundPacketType, SU extends ServerboundPacketType> implements Protocol<CU, CM, SM, SU> {
protected final Class<CU> unmappedClientboundPacketType;
protected final Class<CM> mappedClientboundPacketType;
protected final Class<SM> mappedServerboundPacketType;
protected final Class<SU> unmappedServerboundPacketType;
protected final PacketTypesProvider<CU, CM, SM, SU> packetTypesProvider;
protected final PacketMappings clientboundMappings;
protected final PacketMappings serverboundMappings;
private final Map<Class<?>, Object> storedObjects = new HashMap<>();
private boolean initialized;
@Deprecated
protected AbstractProtocol() {
this(null, null, null, null);
}
/**
* Creates a protocol with automated id mapping if the respective packet type classes are not null.
2023-08-03 10:36:30 +02:00
* They are also required to track the CONFIGURATION state.
*/
2023-02-12 21:14:10 +01:00
protected AbstractProtocol(@Nullable Class<CU> unmappedClientboundPacketType, @Nullable Class<CM> mappedClientboundPacketType,
@Nullable Class<SM> mappedServerboundPacketType, @Nullable Class<SU> unmappedServerboundPacketType) {
this.unmappedClientboundPacketType = unmappedClientboundPacketType;
this.mappedClientboundPacketType = mappedClientboundPacketType;
this.mappedServerboundPacketType = mappedServerboundPacketType;
this.unmappedServerboundPacketType = unmappedServerboundPacketType;
this.packetTypesProvider = createPacketTypesProvider();
this.clientboundMappings = createClientboundPacketMappings();
this.serverboundMappings = createServerboundPacketMappings();
}
@Override
public final void initialize() {
Preconditions.checkArgument(!initialized, "Protocol has already been initialized");
initialized = true;
registerPackets();
registerConfigurationChangeHandlers();
2023-08-06 07:40:15 +02:00
// Register the rest of the ids with no handlers if necessary
if (unmappedClientboundPacketType != null && mappedClientboundPacketType != null
&& unmappedClientboundPacketType != mappedClientboundPacketType) {
registerPacketIdChanges(
packetTypesProvider.unmappedClientboundPacketTypes(),
packetTypesProvider.mappedClientboundPacketTypes(),
this::hasRegisteredClientbound,
this::registerClientbound
);
}
if (mappedServerboundPacketType != null && unmappedServerboundPacketType != null &&
mappedServerboundPacketType != unmappedServerboundPacketType) {
registerPacketIdChanges(
packetTypesProvider.unmappedServerboundPacketTypes(),
packetTypesProvider.mappedServerboundPacketTypes(),
this::hasRegisteredServerbound,
this::registerServerbound
);
}
}
protected void registerConfigurationChangeHandlers() {
// Register handlers for protocol state switching
// Assuming ids will change too often, it is cleaner to register them here instead of the base protocols,
// even if there will be multiple of these handlers
final SU configurationAcknowledgedPacket = configurationAcknowledgedPacket();
if (configurationAcknowledgedPacket != null) {
appendServerbound(configurationAcknowledgedPacket, setClientStateHandler(State.CONFIGURATION));
}
final CU startConfigurationPacket = startConfigurationPacket();
if (startConfigurationPacket != null) {
appendClientbound(startConfigurationPacket, setServerStateHandler(State.CONFIGURATION));
}
final SU finishConfigurationPacket = serverboundFinishConfigurationPacket();
if (finishConfigurationPacket != null) {
appendServerbound(finishConfigurationPacket, setClientStateHandler(State.PLAY));
}
final CU clientboundFinishConfigurationPacket = clientboundFinishConfigurationPacket();
if (clientboundFinishConfigurationPacket != null) {
appendClientbound(clientboundFinishConfigurationPacket, setServerStateHandler(State.PLAY));
}
}
@Override
public void appendClientbound(final CU type, final PacketHandler handler) {
final PacketMapping mapping = clientboundMappings.mappedPacket(type.state(), type.getId());
if (mapping != null) {
mapping.appendHandler(handler);
} else {
registerClientbound(type, handler);
}
}
@Override
public void appendServerbound(final SU type, final PacketHandler handler) {
final PacketMapping mapping = serverboundMappings.mappedPacket(type.state(), type.getId());
if (mapping != null) {
mapping.appendHandler(handler);
} else {
registerServerbound(type, handler);
}
}
private <U extends PacketType, M extends PacketType> void registerPacketIdChanges(
Map<State, PacketTypeMap<U>> unmappedPacketTypes,
Map<State, PacketTypeMap<M>> mappedPacketTypes,
Predicate<U> registeredPredicate,
BiConsumer<U, M> registerConsumer
) {
for (Map.Entry<State, PacketTypeMap<M>> entry : mappedPacketTypes.entrySet()) {
PacketTypeMap<M> mappedTypes = entry.getValue();
2024-01-17 21:04:44 +01:00
PacketTypeMap<U> unmappedTypes = unmappedPacketTypes.get(entry.getKey());
for (U unmappedType : unmappedTypes.types()) {
M mappedType = mappedTypes.typeByName(unmappedType.getName());
if (mappedType == null) {
// No mapped packet of the same name exists
Preconditions.checkArgument(registeredPredicate.test(unmappedType), "Packet %s in %s has no mapping - it needs to be manually cancelled or remapped", unmappedType, getClass());
continue;
}
// Register if no custom handler exists and ids are different
if (unmappedType.getId() != mappedType.getId() && !registeredPredicate.test(unmappedType)) {
registerConsumer.accept(unmappedType, mappedType);
}
}
}
}
@Override
public final void loadMappingData() {
getMappingData().load();
onMappingDataLoaded();
}
/**
* Register the packets for this protocol. To be overriden.
*/
protected void registerPackets() {
2023-02-20 12:02:25 +01:00
callRegister(getEntityRewriter());
callRegister(getItemRewriter());
}
/**
* Called after {@link #loadMappingData()} is called; load extra mapping data for the protocol.
* <p>
* To be overridden if needed.
*/
protected void onMappingDataLoaded() {
2023-02-20 12:02:25 +01:00
callOnMappingDataLoaded(getEntityRewriter());
callOnMappingDataLoaded(getItemRewriter());
callOnMappingDataLoaded(getTagRewriter());
2023-02-20 12:02:25 +01:00
}
private void callRegister(@Nullable Rewriter<?> rewriter) {
if (rewriter != null) {
rewriter.register();
}
}
private void callOnMappingDataLoaded(@Nullable MappingDataListener rewriter) {
2023-02-20 12:02:25 +01:00
if (rewriter != null) {
rewriter.onMappingDataLoaded();
}
}
protected void addEntityTracker(UserConnection connection, EntityTracker tracker) {
Refactor entity tracking and meta handling This essentially merges the two approaches to the metadata handling from ViaVersion and ViaBackwards and improves on both designs. ViaVersion did not track every single entity, but only those needed (at least in theory) and can work with untracked entities' metadata. It had a very simple method overridden by metadata rewriter implementations, directly operating on the full metadata list and manually handling meta index changes as well as item/block/particle id changes. ViaBackwards on the other hand had to track *every single* entity and threw warnings otherwise - while less prone to errors due to giving obvious warnings in the console, it unnecessarily tracks a lot of entities, and those warnings also annoys users when encountering virtual entity plugins (operating asynchronously and sending update packets while already untracked or not yet tracked). Dedicated MetaHandlers made id changes and filtering a lot easier to read and write. However, the actual metadata list handling and its distribution to handlers was not very well implemented and required a lot of list copying and creation as well as exception throws to cancel individual metadata entries. This version has MetaFilters built with a Builder containing multiple helper functions, and the entity tracking is properly given its own map, hashed by a Protocol's class, to be easily and generically accessible from anywhere with only a Protocol class from the UserConnection, along with more optimized metadata list iteration. The entity tracking is largely unchanged, keeping ViaVersion's approach to not having to track *all* entities (and being able to handle null types in meta handlers). All of this is by no means absolutely perfect, but is much less prone to errors than both previous systems and takes a lot less effort to actually write. A last possible change would be to use a primitive int to object map that is built to be concurrency save for the EntityTracker, tho that would have to be chosen carefully.
2021-05-24 23:24:50 +02:00
connection.addEntityTracker(this.getClass(), tracker);
}
2023-02-12 21:14:10 +01:00
protected PacketTypesProvider<CU, CM, SM, SU> createPacketTypesProvider() {
return new SimplePacketTypesProvider<>(
packetTypeMap(unmappedClientboundPacketType, unmappedClientboundPacketType),
packetTypeMap(mappedClientboundPacketType, mappedClientboundPacketType),
packetTypeMap(mappedServerboundPacketType, mappedServerboundPacketType),
packetTypeMap(unmappedServerboundPacketType, unmappedServerboundPacketType)
);
}
protected PacketMappings createClientboundPacketMappings() {
return PacketMappings.arrayMappings();
}
protected PacketMappings createServerboundPacketMappings() {
return PacketMappings.arrayMappings();
}
2023-08-03 10:36:30 +02:00
protected @Nullable SU configurationAcknowledgedPacket() {
return packetTypesProvider.unmappedServerboundType(State.PLAY, "CONFIGURATION_ACKNOWLEDGED");
2023-08-03 10:36:30 +02:00
}
protected @Nullable CU startConfigurationPacket() {
return packetTypesProvider.unmappedClientboundType(State.PLAY, "START_CONFIGURATION");
}
protected @Nullable SU serverboundFinishConfigurationPacket() {
return packetTypesProvider.unmappedServerboundType(State.CONFIGURATION, "FINISH_CONFIGURATION");
}
protected @Nullable CU clientboundFinishConfigurationPacket() {
return packetTypesProvider.unmappedClientboundType(State.CONFIGURATION, "FINISH_CONFIGURATION");
2023-08-06 07:40:15 +02:00
}
// ---------------------------------------------------------------------------------
@Override
public void registerServerbound(State state, int unmappedPacketId, int mappedPacketId, PacketHandler handler, boolean override) {
Preconditions.checkArgument(unmappedPacketId != -1, "Unmapped packet id cannot be -1");
PacketMapping packetMapping = PacketMapping.of(mappedPacketId, handler);
if (!override && serverboundMappings.hasMapping(state, unmappedPacketId)) {
Via.getPlatform().getLogger().log(Level.WARNING, unmappedPacketId + " already registered!" +
" If this override is intentional, set override to true. Stacktrace: ", new Exception());
}
serverboundMappings.addMapping(state, unmappedPacketId, packetMapping);
}
@Override
public void cancelServerbound(State state, int unmappedPacketId) {
registerServerbound(state, unmappedPacketId, unmappedPacketId, PacketWrapper::cancel);
}
@Override
public void registerClientbound(State state, int unmappedPacketId, int mappedPacketId, PacketHandler handler, boolean override) {
Preconditions.checkArgument(unmappedPacketId != -1, "Unmapped packet id cannot be -1");
PacketMapping packetMapping = PacketMapping.of(mappedPacketId, handler);
if (!override && clientboundMappings.hasMapping(state, unmappedPacketId)) {
Via.getPlatform().getLogger().log(Level.WARNING, unmappedPacketId + " already registered!" +
" If override is intentional, set override to true. Stacktrace: ", new Exception());
}
clientboundMappings.addMapping(state, unmappedPacketId, packetMapping);
}
@Override
public void cancelClientbound(State state, int unmappedPacketId) {
registerClientbound(state, unmappedPacketId, unmappedPacketId, PacketWrapper::cancel);
}
// ---------------------------------------------------------------------------------
@Override
2023-02-12 21:14:10 +01:00
public void registerClientbound(CU packetType, @Nullable PacketHandler handler) {
PacketTypeMap<CM> mappedPacketTypes = packetTypesProvider.mappedClientboundPacketTypes().get(packetType.state());
CM mappedPacketType = mappedPacketType(packetType, mappedPacketTypes, unmappedClientboundPacketType, mappedClientboundPacketType);
registerClientbound(packetType, mappedPacketType, handler);
}
@Override
2023-02-12 21:14:10 +01:00
public void registerClientbound(CU packetType, @Nullable CM mappedPacketType, @Nullable PacketHandler handler, boolean override) {
register(clientboundMappings, packetType, mappedPacketType, unmappedClientboundPacketType, mappedClientboundPacketType, handler, override);
}
@Override
2023-02-12 21:14:10 +01:00
public void cancelClientbound(CU packetType) {
registerClientbound(packetType, null, PacketWrapper::cancel);
}
@Override
2023-02-12 21:14:10 +01:00
public void registerServerbound(SU packetType, @Nullable PacketHandler handler) {
PacketTypeMap<SM> mappedPacketTypes = packetTypesProvider.mappedServerboundPacketTypes().get(packetType.state());
SM mappedPacketType = mappedPacketType(packetType, mappedPacketTypes, unmappedServerboundPacketType, mappedServerboundPacketType);
registerServerbound(packetType, mappedPacketType, handler);
}
@Override
2023-02-12 21:14:10 +01:00
public void registerServerbound(SU packetType, @Nullable SM mappedPacketType, @Nullable PacketHandler handler, boolean override) {
register(serverboundMappings, packetType, mappedPacketType, unmappedServerboundPacketType, mappedServerboundPacketType, handler, override);
}
@Override
2023-02-12 21:14:10 +01:00
public void cancelServerbound(SU packetType) {
registerServerbound(packetType, null, PacketWrapper::cancel);
}
private void register(PacketMappings packetMappings, PacketType packetType, @Nullable PacketType mappedPacketType,
Class<? extends PacketType> unmappedPacketClass, Class<? extends PacketType> mappedPacketClass,
@Nullable PacketHandler handler, boolean override) {
checkPacketType(packetType, unmappedPacketClass == null || unmappedPacketClass.isInstance(packetType));
if (mappedPacketType != null) {
checkPacketType(mappedPacketType, mappedPacketClass == null || mappedPacketClass.isInstance(mappedPacketType));
Preconditions.checkArgument(packetType.state() == mappedPacketType.state(),
"Packet type state does not match mapped packet type state");
Preconditions.checkArgument(packetType.direction() == mappedPacketType.direction(),
"Packet type direction does not match mapped packet type state");
}
PacketMapping packetMapping = PacketMapping.of(mappedPacketType, handler);
if (!override && packetMappings.hasMapping(packetType)) {
Via.getPlatform().getLogger().log(Level.WARNING, packetType + " already registered!" +
" If override is intentional, set override to true. Stacktrace: ", new Exception());
}
packetMappings.addMapping(packetType, packetMapping);
}
private static <U extends PacketType, M extends PacketType> M mappedPacketType(U packetType, PacketTypeMap<M> mappedTypes, Class<U> unmappedPacketTypeClass, Class<M> mappedPacketTypeClass) {
Preconditions.checkNotNull(packetType);
checkPacketType(packetType, unmappedPacketTypeClass == null || unmappedPacketTypeClass.isInstance(packetType));
if (unmappedPacketTypeClass == mappedPacketTypeClass) {
//noinspection unchecked
return (M) packetType;
}
Preconditions.checkNotNull(mappedTypes, "Mapped packet types not provided for state %s of type class %s", packetType.state(), mappedPacketTypeClass);
M mappedType = mappedTypes.typeByName(packetType.getName());
if (mappedType != null) {
return mappedType;
}
throw new IllegalArgumentException("Packet type " + packetType + " in " + packetType.getClass().getSimpleName() + " could not be automatically mapped!");
}
@Override
public boolean hasRegisteredClientbound(State state, int unmappedPacketId) {
return clientboundMappings.hasMapping(state, unmappedPacketId);
}
@Override
public boolean hasRegisteredServerbound(State state, int unmappedPacketId) {
return serverboundMappings.hasMapping(state, unmappedPacketId);
}
@Override
public void transform(Direction direction, State state, PacketWrapper packetWrapper) throws Exception {
PacketMappings mappings = direction == Direction.CLIENTBOUND ? clientboundMappings : serverboundMappings;
int unmappedId = packetWrapper.getId();
PacketMapping packetMapping = mappings.mappedPacket(state, unmappedId);
if (packetMapping == null) {
return;
}
// Change packet id and apply remapping
packetMapping.applyType(packetWrapper);
PacketHandler handler = packetMapping.handler();
if (handler != null) {
try {
handler.handle(packetWrapper);
} catch (CancelException e) {
throw e; // Pass through CancelExceptions
} catch (InformativeException e) {
e.addSource(handler.getClass());
printRemapError(direction, state, unmappedId, packetWrapper.getId(), e);
throw e;
} catch (Exception e) {
// Wrap other exceptions during packet handling
InformativeException ex = new InformativeException(e);
ex.addSource(handler.getClass());
printRemapError(direction, state, unmappedId, packetWrapper.getId(), ex);
throw ex;
}
if (packetWrapper.isCancelled()) {
throw CancelException.generate();
}
}
}
private void printRemapError(Direction direction, State state, int unmappedPacketId, int mappedPacketId, InformativeException e) {
2023-01-06 20:33:17 +01:00
// Don't print errors during handshake/login/status
if (state != State.PLAY && direction == Direction.SERVERBOUND && !Via.getManager().debugHandler().enabled()) {
e.setShouldBePrinted(false);
return;
}
PacketType packetType = direction == Direction.CLIENTBOUND
? packetTypesProvider.unmappedClientboundType(state, unmappedPacketId)
: packetTypesProvider.unmappedServerboundType(state, unmappedPacketId);
if (packetType != null) {
Via.getPlatform().getLogger().warning("ERROR IN " + getClass().getSimpleName() + " IN REMAP OF " + packetType + " (" + ProtocolUtil.toNiceHex(unmappedPacketId) + ")");
} else {
Via.getPlatform().getLogger().warning("ERROR IN " + getClass().getSimpleName()
+ " IN REMAP OF " + state + " " + ProtocolUtil.toNiceHex(unmappedPacketId) + "->" + ProtocolUtil.toNiceHex(mappedPacketId));
}
}
/**
* @param packetType packet type
* @param isValid expression to check the packet's validity
* @throws IllegalArgumentException if the given expression is not met
*/
private static void checkPacketType(PacketType packetType, boolean isValid) {
if (!isValid) {
throw new IllegalArgumentException("Packet type " + packetType + " in " + packetType.getClass().getSimpleName() + " is taken from the wrong packet types class");
}
}
private PacketHandler setClientStateHandler(final State state) {
return wrapper -> wrapper.user().getProtocolInfo().setClientState(state);
}
private PacketHandler setServerStateHandler(final State state) {
2023-10-05 10:22:13 +02:00
return wrapper -> wrapper.user().getProtocolInfo().setServerState(state);
}
2023-02-24 10:07:25 +01:00
@Override
public final PacketTypesProvider<CU, CM, SM, SU> getPacketTypesProvider() {
2023-02-24 10:07:25 +01:00
return packetTypesProvider;
}
@Override
public @Nullable <T> T get(Class<T> objectClass) {
2023-01-06 20:33:17 +01:00
//noinspection unchecked
return (T) storedObjects.get(objectClass);
}
@Override
public void put(Object object) {
storedObjects.put(object.getClass(), object);
}
@Override
public String toString() {
return "Protocol:" + getClass().getSimpleName();
}
}