mirror of
https://github.com/Xiao-MoMi/craft-engine.git
synced 2025-12-25 18:09:27 +00:00
refactor(network): 重构注册到注册表
This commit is contained in:
@@ -1,18 +0,0 @@
|
||||
package net.momirealms.craftengine.bukkit.plugin.network.payload;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.codec.NetworkCodec;
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.codec.NetworkDecoder;
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.codec.NetworkMemberEncoder;
|
||||
import net.momirealms.craftengine.core.plugin.network.NetWorkUser;
|
||||
|
||||
public interface Data {
|
||||
|
||||
default void handle(NetWorkUser user) {
|
||||
}
|
||||
|
||||
static <B extends ByteBuf, T extends Data> NetworkCodec<B, T> codec(NetworkMemberEncoder<B, T> networkMemberEncoder, NetworkDecoder<B, T> networkDecoder) {
|
||||
return NetworkCodec.ofMember(networkMemberEncoder, networkDecoder);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,55 +1,44 @@
|
||||
package net.momirealms.craftengine.bukkit.plugin.network.payload;
|
||||
|
||||
import io.netty.buffer.Unpooled;
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.codec.NetworkCodec;
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.protocol.CancelBlockUpdateData;
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.protocol.ClientBlockStateSizeData;
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.protocol.ClientCustomBlockData;
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.protocol.CancelBlockUpdatePacket;
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.protocol.ClientBlockStateSizePacket;
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.protocol.ClientCustomBlockPacket;
|
||||
import net.momirealms.craftengine.core.plugin.CraftEngine;
|
||||
import net.momirealms.craftengine.core.plugin.network.ModPacket;
|
||||
import net.momirealms.craftengine.core.plugin.network.NetWorkUser;
|
||||
import net.momirealms.craftengine.core.plugin.network.NetworkManager;
|
||||
import net.momirealms.craftengine.core.plugin.network.codec.NetworkCodec;
|
||||
import net.momirealms.craftengine.core.registry.BuiltInRegistries;
|
||||
import net.momirealms.craftengine.core.registry.Holder;
|
||||
import net.momirealms.craftengine.core.registry.WritableRegistry;
|
||||
import net.momirealms.craftengine.core.util.FriendlyByteBuf;
|
||||
import net.momirealms.craftengine.core.util.ResourceKey;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.Optional;
|
||||
|
||||
public class PayloadHelper {
|
||||
private static final Map<Class<Data>, Byte> classToType = new HashMap<>();
|
||||
private static final Map<Byte, NetworkCodec<FriendlyByteBuf, Data>> typeToCodec = new HashMap<>();
|
||||
private static final AtomicInteger typeCounter = new AtomicInteger(0);
|
||||
|
||||
public static void registerDataTypes() {
|
||||
registerDataType(ClientCustomBlockData.class, ClientCustomBlockData.CODEC);
|
||||
registerDataType(CancelBlockUpdateData.class, CancelBlockUpdateData.CODEC);
|
||||
registerDataType(ClientBlockStateSizeData.class, ClientBlockStateSizeData.CODEC);
|
||||
registerDataType(ClientCustomBlockPacket.TYPE, ClientCustomBlockPacket.CODEC);
|
||||
registerDataType(CancelBlockUpdatePacket.TYPE, CancelBlockUpdatePacket.CODEC);
|
||||
registerDataType(ClientBlockStateSizePacket.TYPE, ClientBlockStateSizePacket.CODEC);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T extends Data> void registerDataType(Class<T> dataClass, NetworkCodec<FriendlyByteBuf, T> codec) {
|
||||
if (classToType.containsKey(dataClass)) {
|
||||
CraftEngine.instance().logger().warn("Duplicate data type class: " + dataClass.getName());
|
||||
return;
|
||||
}
|
||||
int next = typeCounter.getAndIncrement();
|
||||
if (next > 255) {
|
||||
throw new IllegalStateException("Too many data types registered, byte index overflow (max 256)");
|
||||
}
|
||||
byte type = (byte) next;
|
||||
classToType.put((Class<Data>) dataClass, type);
|
||||
typeToCodec.put(type, (NetworkCodec<FriendlyByteBuf, Data>) codec);
|
||||
public static <T extends ModPacket> void registerDataType(ResourceKey<NetworkCodec<FriendlyByteBuf, ? extends ModPacket>> key, NetworkCodec<FriendlyByteBuf, T> codec) {
|
||||
((WritableRegistry<NetworkCodec<FriendlyByteBuf, ? extends ModPacket>>) BuiltInRegistries.MOD_PACKET).register(key, codec);
|
||||
}
|
||||
|
||||
public static void sendData(NetWorkUser user, Data data) {
|
||||
Class<? extends Data> dataClass = data.getClass();
|
||||
Byte type = classToType.get(dataClass);
|
||||
if (type == null) {
|
||||
CraftEngine.instance().logger().warn("Unknown data type class: " + dataClass.getName());
|
||||
public static void sendData(NetWorkUser user, ModPacket data) {
|
||||
Optional<Holder.Reference<NetworkCodec<FriendlyByteBuf, ? extends ModPacket>>> optionalType = BuiltInRegistries.MOD_PACKET.get(data.type());
|
||||
if (optionalType.isEmpty()) {
|
||||
CraftEngine.instance().logger().warn("Unknown data type class: " + data.getClass().getName());
|
||||
return;
|
||||
}
|
||||
NetworkCodec<FriendlyByteBuf, Data> codec = typeToCodec.get(type);
|
||||
@SuppressWarnings("unchecked")
|
||||
NetworkCodec<FriendlyByteBuf, ModPacket> codec = (NetworkCodec<FriendlyByteBuf, ModPacket>) optionalType.get().value();
|
||||
FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer());
|
||||
buf.writeByte(type);
|
||||
buf.writeByte(BuiltInRegistries.MOD_PACKET.getId(codec));
|
||||
codec.encode(buf, data);
|
||||
user.sendCustomPayload(NetworkManager.MOD_CHANNEL_KEY, buf.array());
|
||||
}
|
||||
@@ -57,13 +46,14 @@ public class PayloadHelper {
|
||||
public static void handleReceiver(Payload payload, NetWorkUser user) {
|
||||
FriendlyByteBuf buf = payload.toBuffer();
|
||||
byte type = buf.readByte();
|
||||
NetworkCodec<FriendlyByteBuf, Data> codec = typeToCodec.get(type);
|
||||
@SuppressWarnings("unchecked")
|
||||
NetworkCodec<FriendlyByteBuf, ModPacket> codec = (NetworkCodec<FriendlyByteBuf, ModPacket>) BuiltInRegistries.MOD_PACKET.getValue(type);
|
||||
if (codec == null) {
|
||||
CraftEngine.instance().logger().warn("Unknown data type received: " + type);
|
||||
return;
|
||||
}
|
||||
|
||||
Data networkData = codec.decode(buf);
|
||||
ModPacket networkData = codec.decode(buf);
|
||||
networkData.handle(user);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
package net.momirealms.craftengine.bukkit.plugin.network.payload.codec;
|
||||
|
||||
public interface NetworkDecoder<I, T> {
|
||||
T decode(I in);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package net.momirealms.craftengine.bukkit.plugin.network.payload.protocol;
|
||||
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.Data;
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.PayloadHelper;
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.codec.NetworkCodec;
|
||||
import net.momirealms.craftengine.core.plugin.network.NetWorkUser;
|
||||
import net.momirealms.craftengine.core.util.FriendlyByteBuf;
|
||||
|
||||
public record CancelBlockUpdateData(boolean enabled) implements Data {
|
||||
public static final NetworkCodec<FriendlyByteBuf, CancelBlockUpdateData> CODEC = Data.codec(
|
||||
CancelBlockUpdateData::encode,
|
||||
CancelBlockUpdateData::new
|
||||
);
|
||||
|
||||
private CancelBlockUpdateData(FriendlyByteBuf buf) {
|
||||
this(buf.readBoolean());
|
||||
}
|
||||
|
||||
private void encode(FriendlyByteBuf buf) {
|
||||
buf.writeBoolean(this.enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(NetWorkUser user) {
|
||||
if (!this.enabled) return;
|
||||
PayloadHelper.sendData(user, this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package net.momirealms.craftengine.bukkit.plugin.network.payload.protocol;
|
||||
|
||||
import net.momirealms.craftengine.core.plugin.network.ModPacket;
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.PayloadHelper;
|
||||
import net.momirealms.craftengine.core.plugin.network.codec.NetworkCodec;
|
||||
import net.momirealms.craftengine.core.plugin.network.NetWorkUser;
|
||||
import net.momirealms.craftengine.core.registry.BuiltInRegistries;
|
||||
import net.momirealms.craftengine.core.util.FriendlyByteBuf;
|
||||
import net.momirealms.craftengine.core.util.Key;
|
||||
import net.momirealms.craftengine.core.util.ResourceKey;
|
||||
|
||||
public record CancelBlockUpdatePacket(boolean enabled) implements ModPacket {
|
||||
public static final ResourceKey<NetworkCodec<FriendlyByteBuf, ? extends ModPacket>> TYPE = ResourceKey.create(
|
||||
BuiltInRegistries.MOD_PACKET.key().location(), Key.of("craftengine", "cancel_block_update")
|
||||
);
|
||||
public static final NetworkCodec<FriendlyByteBuf, CancelBlockUpdatePacket> CODEC = ModPacket.codec(
|
||||
CancelBlockUpdatePacket::encode,
|
||||
CancelBlockUpdatePacket::new
|
||||
);
|
||||
|
||||
private CancelBlockUpdatePacket(FriendlyByteBuf buf) {
|
||||
this(buf.readBoolean());
|
||||
}
|
||||
|
||||
private void encode(FriendlyByteBuf buf) {
|
||||
buf.writeBoolean(this.enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceKey<NetworkCodec<FriendlyByteBuf, ? extends ModPacket>> type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(NetWorkUser user) {
|
||||
if (!this.enabled) return;
|
||||
PayloadHelper.sendData(user, this);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package net.momirealms.craftengine.bukkit.plugin.network.payload.protocol;
|
||||
|
||||
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.Data;
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.codec.NetworkCodec;
|
||||
import net.momirealms.craftengine.core.plugin.network.NetWorkUser;
|
||||
import net.momirealms.craftengine.core.util.FriendlyByteBuf;
|
||||
import net.momirealms.craftengine.core.util.IntIdentityList;
|
||||
|
||||
public record ClientBlockStateSizeData(int blockStateSize) implements Data {
|
||||
public static final NetworkCodec<FriendlyByteBuf, ClientBlockStateSizeData> CODEC = Data.codec(
|
||||
ClientBlockStateSizeData::encode,
|
||||
ClientBlockStateSizeData::new
|
||||
);
|
||||
|
||||
private ClientBlockStateSizeData(FriendlyByteBuf buf) {
|
||||
this(buf.readInt());
|
||||
}
|
||||
|
||||
private void encode(FriendlyByteBuf buf) {
|
||||
buf.writeInt(this.blockStateSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(NetWorkUser user) {
|
||||
user.setClientBlockList(new IntIdentityList(this.blockStateSize));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package net.momirealms.craftengine.bukkit.plugin.network.payload.protocol;
|
||||
|
||||
|
||||
import net.momirealms.craftengine.core.plugin.network.ModPacket;
|
||||
import net.momirealms.craftengine.core.plugin.network.codec.NetworkCodec;
|
||||
import net.momirealms.craftengine.core.plugin.network.NetWorkUser;
|
||||
import net.momirealms.craftengine.core.registry.BuiltInRegistries;
|
||||
import net.momirealms.craftengine.core.util.FriendlyByteBuf;
|
||||
import net.momirealms.craftengine.core.util.IntIdentityList;
|
||||
import net.momirealms.craftengine.core.util.Key;
|
||||
import net.momirealms.craftengine.core.util.ResourceKey;
|
||||
|
||||
public record ClientBlockStateSizePacket(int blockStateSize) implements ModPacket {
|
||||
public static final ResourceKey<NetworkCodec<FriendlyByteBuf, ? extends ModPacket>> TYPE = ResourceKey.create(
|
||||
BuiltInRegistries.MOD_PACKET.key().location(), Key.of("craftengine", "client_block_state_size")
|
||||
);
|
||||
public static final NetworkCodec<FriendlyByteBuf, ClientBlockStateSizePacket> CODEC = ModPacket.codec(
|
||||
ClientBlockStateSizePacket::encode,
|
||||
ClientBlockStateSizePacket::new
|
||||
);
|
||||
|
||||
private ClientBlockStateSizePacket(FriendlyByteBuf buf) {
|
||||
this(buf.readInt());
|
||||
}
|
||||
|
||||
private void encode(FriendlyByteBuf buf) {
|
||||
buf.writeInt(this.blockStateSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceKey<NetworkCodec<FriendlyByteBuf, ? extends ModPacket>> type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(NetWorkUser user) {
|
||||
user.setClientBlockList(new IntIdentityList(this.blockStateSize));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,20 +3,26 @@ package net.momirealms.craftengine.bukkit.plugin.network.payload.protocol;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.TranslationArgument;
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.Data;
|
||||
import net.momirealms.craftengine.bukkit.plugin.network.payload.codec.NetworkCodec;
|
||||
import net.momirealms.craftengine.core.plugin.network.ModPacket;
|
||||
import net.momirealms.craftengine.core.plugin.network.codec.NetworkCodec;
|
||||
import net.momirealms.craftengine.bukkit.util.RegistryUtils;
|
||||
import net.momirealms.craftengine.core.plugin.network.NetWorkUser;
|
||||
import net.momirealms.craftengine.core.registry.BuiltInRegistries;
|
||||
import net.momirealms.craftengine.core.util.FriendlyByteBuf;
|
||||
import net.momirealms.craftengine.core.util.IntIdentityList;
|
||||
import net.momirealms.craftengine.core.util.Key;
|
||||
import net.momirealms.craftengine.core.util.ResourceKey;
|
||||
|
||||
public record ClientCustomBlockData(int size) implements Data {
|
||||
public static final NetworkCodec<FriendlyByteBuf, ClientCustomBlockData> CODEC = Data.codec(
|
||||
ClientCustomBlockData::encode,
|
||||
ClientCustomBlockData::new
|
||||
public record ClientCustomBlockPacket(int size) implements ModPacket {
|
||||
public static final ResourceKey<NetworkCodec<FriendlyByteBuf, ? extends ModPacket>> TYPE = ResourceKey.create(
|
||||
BuiltInRegistries.MOD_PACKET.key().location(), Key.of("craftengine", "client_custom_block")
|
||||
);
|
||||
public static final NetworkCodec<FriendlyByteBuf, ClientCustomBlockPacket> CODEC = ModPacket.codec(
|
||||
ClientCustomBlockPacket::encode,
|
||||
ClientCustomBlockPacket::new
|
||||
);
|
||||
|
||||
private ClientCustomBlockData(FriendlyByteBuf buf) {
|
||||
private ClientCustomBlockPacket(FriendlyByteBuf buf) {
|
||||
this(buf.readInt());
|
||||
}
|
||||
|
||||
@@ -24,6 +30,11 @@ public record ClientCustomBlockData(int size) implements Data {
|
||||
buf.writeInt(this.size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResourceKey<NetworkCodec<FriendlyByteBuf, ? extends ModPacket>> type() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(NetWorkUser user) {
|
||||
int serverBlockRegistrySize = RegistryUtils.currentBlockRegistrySize();
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.momirealms.craftengine.core.plugin.network;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.momirealms.craftengine.core.plugin.network.codec.NetworkCodec;
|
||||
import net.momirealms.craftengine.core.plugin.network.codec.NetworkDecoder;
|
||||
import net.momirealms.craftengine.core.plugin.network.codec.NetworkMemberEncoder;
|
||||
import net.momirealms.craftengine.core.util.FriendlyByteBuf;
|
||||
import net.momirealms.craftengine.core.util.ResourceKey;
|
||||
|
||||
public interface ModPacket {
|
||||
|
||||
ResourceKey<NetworkCodec<FriendlyByteBuf, ? extends ModPacket>> type();
|
||||
|
||||
default void handle(NetWorkUser user) {
|
||||
}
|
||||
|
||||
static <B extends ByteBuf, T extends ModPacket> NetworkCodec<B, T> codec(NetworkMemberEncoder<B, T> networkMemberEncoder, NetworkDecoder<B, T> networkDecoder) {
|
||||
return NetworkCodec.ofMember(networkMemberEncoder, networkDecoder);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.momirealms.craftengine.bukkit.plugin.network.payload.codec;
|
||||
package net.momirealms.craftengine.core.plugin.network.codec;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.momirealms.craftengine.bukkit.plugin.network.payload.codec;
|
||||
package net.momirealms.craftengine.core.plugin.network.codec;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufInputStream;
|
||||
@@ -0,0 +1,5 @@
|
||||
package net.momirealms.craftengine.core.plugin.network.codec;
|
||||
|
||||
public interface NetworkDecoder<I, T> {
|
||||
T decode(I in);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.momirealms.craftengine.bukkit.plugin.network.payload.codec;
|
||||
package net.momirealms.craftengine.core.plugin.network.codec;
|
||||
|
||||
public interface NetworkEncoder<O, T> {
|
||||
void encode(O out, T value);
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.momirealms.craftengine.bukkit.plugin.network.payload.codec;
|
||||
package net.momirealms.craftengine.core.plugin.network.codec;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface NetworkMemberEncoder<O, T> {
|
||||
@@ -40,6 +40,9 @@ import net.momirealms.craftengine.core.plugin.context.condition.ConditionFactory
|
||||
import net.momirealms.craftengine.core.plugin.context.function.FunctionFactory;
|
||||
import net.momirealms.craftengine.core.plugin.context.number.NumberProviderFactory;
|
||||
import net.momirealms.craftengine.core.plugin.context.selector.PlayerSelectorFactory;
|
||||
import net.momirealms.craftengine.core.plugin.network.ModPacket;
|
||||
import net.momirealms.craftengine.core.plugin.network.codec.NetworkCodec;
|
||||
import net.momirealms.craftengine.core.util.FriendlyByteBuf;
|
||||
import net.momirealms.craftengine.core.util.ResourceKey;
|
||||
|
||||
public class BuiltInRegistries {
|
||||
@@ -81,6 +84,7 @@ public class BuiltInRegistries {
|
||||
public static final Registry<LegacyRecipe.Type> LEGACY_RECIPE_TYPE = createConstantBoundRegistry(Registries.LEGACY_RECIPE_TYPE);
|
||||
public static final Registry<PostProcessor.Type<?>> RECIPE_POST_PROCESSOR_TYPE = createConstantBoundRegistry(Registries.RECIPE_POST_PROCESSOR_TYPE);
|
||||
public static final Registry<ItemUpdaterType<?>> ITEM_UPDATER_TYPE = createConstantBoundRegistry(Registries.ITEM_UPDATER_TYPE);
|
||||
public static final Registry<NetworkCodec<FriendlyByteBuf, ? extends ModPacket>> MOD_PACKET = createConstantBoundRegistry(Registries.MOD_PACKET);
|
||||
|
||||
private static <T> Registry<T> createConstantBoundRegistry(ResourceKey<? extends Registry<T>> key) {
|
||||
return new ConstantBoundRegistry<>(key);
|
||||
|
||||
@@ -40,6 +40,9 @@ import net.momirealms.craftengine.core.plugin.context.condition.ConditionFactory
|
||||
import net.momirealms.craftengine.core.plugin.context.function.FunctionFactory;
|
||||
import net.momirealms.craftengine.core.plugin.context.number.NumberProviderFactory;
|
||||
import net.momirealms.craftengine.core.plugin.context.selector.PlayerSelectorFactory;
|
||||
import net.momirealms.craftengine.core.plugin.network.ModPacket;
|
||||
import net.momirealms.craftengine.core.plugin.network.codec.NetworkCodec;
|
||||
import net.momirealms.craftengine.core.util.FriendlyByteBuf;
|
||||
import net.momirealms.craftengine.core.util.Key;
|
||||
import net.momirealms.craftengine.core.util.ResourceKey;
|
||||
|
||||
@@ -83,4 +86,5 @@ public class Registries {
|
||||
public static final ResourceKey<Registry<LegacyRecipe.Type>> LEGACY_RECIPE_TYPE = ResourceKey.create(ROOT_REGISTRY, Key.withDefaultNamespace("legacy_recipe_type"));
|
||||
public static final ResourceKey<Registry<PostProcessor.Type<?>>> RECIPE_POST_PROCESSOR_TYPE = ResourceKey.create(ROOT_REGISTRY, Key.withDefaultNamespace("recipe_post_processor_type"));
|
||||
public static final ResourceKey<Registry<ItemUpdaterType<?>>> ITEM_UPDATER_TYPE = ResourceKey.create(ROOT_REGISTRY, Key.withDefaultNamespace("item_updater_type"));
|
||||
public static final ResourceKey<Registry<NetworkCodec<FriendlyByteBuf, ? extends ModPacket>>> MOD_PACKET = ResourceKey.create(ROOT_REGISTRY, Key.withDefaultNamespace("mod_packet"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user