13
0
geforkt von Mirrors/Velocity

Player has an identity

Dieser Commit ist enthalten in:
Riley Park 2020-10-12 16:47:49 -07:00
Ursprung 3b1009caba
Commit 60e917b4a1
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: D831AF236C834E45
12 geänderte Dateien mit 69 neuen und 64 gelöschten Zeilen

Datei anzeigen

@ -1,7 +1,9 @@
package com.velocitypowered.api.command; package com.velocitypowered.api.command;
import com.velocitypowered.api.permission.PermissionSubject; import com.velocitypowered.api.permission.PermissionSubject;
import com.velocitypowered.api.proxy.ProxyAudience; import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.audience.MessageType;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacytext3.LegacyText3ComponentSerializer; import net.kyori.adventure.text.serializer.legacytext3.LegacyText3ComponentSerializer;
import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.NonNull;
@ -9,7 +11,7 @@ import org.checkerframework.checker.nullness.qual.NonNull;
/** /**
* Represents something that can be used to run a {@link Command}. * Represents something that can be used to run a {@link Command}.
*/ */
public interface CommandSource extends PermissionSubject, ProxyAudience { public interface CommandSource extends Audience, PermissionSubject {
/** /**
* Sends the specified {@code component} to the invoker. * Sends the specified {@code component} to the invoker.
@ -21,7 +23,8 @@ public interface CommandSource extends PermissionSubject, ProxyAudience {
void sendMessage(net.kyori.text.Component component); void sendMessage(net.kyori.text.Component component);
@Override @Override
default void sendMessage(@NonNull Component message) { default void sendMessage(@NonNull Identity identity, @NonNull Component message,
@NonNull MessageType type) {
this.sendMessage(LegacyText3ComponentSerializer.get().serialize(message)); this.sendMessage(LegacyText3ComponentSerializer.get().serialize(message));
} }
} }

Datei anzeigen

@ -14,13 +14,14 @@ import com.velocitypowered.api.util.title.Title;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import net.kyori.adventure.identity.Identified;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
/** /**
* Represents a player who is connected to the proxy. * Represents a player who is connected to the proxy.
*/ */
public interface Player extends CommandSource, InboundConnection, ChannelMessageSource, public interface Player extends CommandSource, Identified, InboundConnection,
ChannelMessageSink { ChannelMessageSource, ChannelMessageSink {
/** /**
* Returns the player's current username. * Returns the player's current username.

Datei anzeigen

@ -1,25 +0,0 @@
package com.velocitypowered.api.proxy;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.audience.MessageType;
import net.kyori.adventure.text.Component;
import org.checkerframework.checker.nullness.qual.NonNull;
/**
* Indicates an {@link Audience} that is on the proxy. This interface contains no-op default methods
* that are used to bridge compatibility issues with the new adventure API. This interface will go
* away in Velocity 2.0.0.
*
* @deprecated Only used to handle compatibility problems, will go away in Velocity 2.0.0
*/
@Deprecated
public interface ProxyAudience extends Audience {
@Override
void sendMessage(@NonNull Component message);
@Override
default void sendMessage(@NonNull Component message, @NonNull MessageType type) {
sendMessage(message);
}
}

Datei anzeigen

@ -24,6 +24,7 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
@ -162,7 +163,7 @@ public class VelocityCommandManager implements CommandManager {
boolean isSyntaxError = !e.getType().equals( boolean isSyntaxError = !e.getType().equals(
CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand()); CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand());
if (isSyntaxError) { if (isSyntaxError) {
source.sendMessage(Component.text(e.getMessage(), NamedTextColor.RED)); source.sendMessage(Identity.nil(), Component.text(e.getMessage(), NamedTextColor.RED));
// This is, of course, a lie, but the API will need to change... // This is, of course, a lie, but the API will need to change...
return true; return true;
} else { } else {

Datei anzeigen

@ -17,6 +17,7 @@ import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.server.RegisteredServer; import com.velocitypowered.api.proxy.server.RegisteredServer;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
@ -59,7 +60,7 @@ public class GlistCommand {
private int totalCount(final CommandContext<CommandSource> context) { private int totalCount(final CommandContext<CommandSource> context) {
final CommandSource source = context.getSource(); final CommandSource source = context.getSource();
sendTotalProxyCount(source); sendTotalProxyCount(source);
source.sendMessage( source.sendMessage(Identity.nil(),
Component.text().content("To view all players on servers, use ") Component.text().content("To view all players on servers, use ")
.color(NamedTextColor.YELLOW) .color(NamedTextColor.YELLOW)
.append(Component.text("/glist all", NamedTextColor.DARK_AQUA)) .append(Component.text("/glist all", NamedTextColor.DARK_AQUA))
@ -79,7 +80,7 @@ public class GlistCommand {
} else { } else {
Optional<RegisteredServer> registeredServer = server.getServer(serverName); Optional<RegisteredServer> registeredServer = server.getServer(serverName);
if (!registeredServer.isPresent()) { if (!registeredServer.isPresent()) {
source.sendMessage( source.sendMessage(Identity.nil(),
Component.text("Server " + serverName + " doesn't exist.", NamedTextColor.RED)); Component.text("Server " + serverName + " doesn't exist.", NamedTextColor.RED));
return -1; return -1;
} }
@ -89,7 +90,8 @@ public class GlistCommand {
} }
private void sendTotalProxyCount(CommandSource target) { private void sendTotalProxyCount(CommandSource target) {
target.sendMessage(Component.text().content("There are ").color(NamedTextColor.YELLOW) target.sendMessage(Identity.nil(), Component.text()
.content("There are ").color(NamedTextColor.YELLOW)
.append(Component.text(server.getAllPlayers().size(), NamedTextColor.GREEN)) .append(Component.text(server.getAllPlayers().size(), NamedTextColor.GREEN))
.append(Component.text(" player(s) online.", NamedTextColor.YELLOW)) .append(Component.text(" player(s) online.", NamedTextColor.YELLOW))
.build()); .build());
@ -117,6 +119,6 @@ public class GlistCommand {
} }
} }
target.sendMessage(builder.build()); target.sendMessage(Identity.nil(), builder.build());
} }
} }

Datei anzeigen

@ -15,6 +15,7 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.ClickEvent;
@ -35,7 +36,7 @@ public class ServerCommand implements SimpleCommand {
final String[] args = invocation.arguments(); final String[] args = invocation.arguments();
if (!(source instanceof Player)) { if (!(source instanceof Player)) {
source.sendMessage(Component.text("Only players may run this command.", source.sendMessage(Identity.nil(), Component.text("Only players may run this command.",
NamedTextColor.RED)); NamedTextColor.RED));
return; return;
} }
@ -46,7 +47,7 @@ public class ServerCommand implements SimpleCommand {
String serverName = args[0]; String serverName = args[0];
Optional<RegisteredServer> toConnect = server.getServer(serverName); Optional<RegisteredServer> toConnect = server.getServer(serverName);
if (!toConnect.isPresent()) { if (!toConnect.isPresent()) {
player.sendMessage( player.sendMessage(Identity.nil(),
Component.text("Server " + serverName + " doesn't exist.", NamedTextColor.RED)); Component.text("Server " + serverName + " doesn't exist.", NamedTextColor.RED));
return; return;
} }
@ -60,12 +61,12 @@ public class ServerCommand implements SimpleCommand {
private void outputServerInformation(Player executor) { private void outputServerInformation(Player executor) {
String currentServer = executor.getCurrentServer().map(ServerConnection::getServerInfo) String currentServer = executor.getCurrentServer().map(ServerConnection::getServerInfo)
.map(ServerInfo::getName).orElse("<unknown>"); .map(ServerInfo::getName).orElse("<unknown>");
executor.sendMessage(Component.text("You are currently connected to " + currentServer + ".", executor.sendMessage(Identity.nil(), Component.text(
NamedTextColor.YELLOW)); "You are currently connected to " + currentServer + ".", NamedTextColor.YELLOW));
List<RegisteredServer> servers = BuiltinCommandUtil.sortedServerList(server); List<RegisteredServer> servers = BuiltinCommandUtil.sortedServerList(server);
if (servers.size() > MAX_SERVERS_TO_LIST) { if (servers.size() > MAX_SERVERS_TO_LIST) {
executor.sendMessage(Component.text( executor.sendMessage(Identity.nil(), Component.text(
"Too many servers to list. Tab-complete to show all servers.", NamedTextColor.RED)); "Too many servers to list. Tab-complete to show all servers.", NamedTextColor.RED));
return; return;
} }
@ -81,7 +82,7 @@ public class ServerCommand implements SimpleCommand {
} }
} }
executor.sendMessage(serverListBuilder.build()); executor.sendMessage(Identity.nil(), serverListBuilder.build());
} }
private TextComponent formatServerComponent(String currentPlayerServer, RegisteredServer server) { private TextComponent formatServerComponent(String currentPlayerServer, RegisteredServer server) {

Datei anzeigen

@ -16,6 +16,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.ClickEvent;
@ -60,7 +61,7 @@ public class VelocityCommand implements SimpleCommand {
.map(Map.Entry::getKey) .map(Map.Entry::getKey)
.collect(Collectors.joining("|")); .collect(Collectors.joining("|"));
String commandText = "/velocity <" + availableCommands + ">"; String commandText = "/velocity <" + availableCommands + ">";
source.sendMessage(Component.text(commandText, NamedTextColor.RED)); source.sendMessage(Identity.nil(), Component.text(commandText, NamedTextColor.RED));
} }
@Override @Override
@ -143,15 +144,16 @@ public class VelocityCommand implements SimpleCommand {
public void execute(CommandSource source, String @NonNull [] args) { public void execute(CommandSource source, String @NonNull [] args) {
try { try {
if (server.reloadConfiguration()) { if (server.reloadConfiguration()) {
source.sendMessage(Component.text("Configuration reloaded.", NamedTextColor.GREEN)); source.sendMessage(Identity.nil(), Component.text(
"Configuration reloaded.", NamedTextColor.GREEN));
} else { } else {
source.sendMessage(Component.text( source.sendMessage(Identity.nil(), Component.text(
"Unable to reload your configuration. Check the console for more details.", "Unable to reload your configuration. Check the console for more details.",
NamedTextColor.RED)); NamedTextColor.RED));
} }
} catch (Exception e) { } catch (Exception e) {
logger.error("Unable to reload configuration", e); logger.error("Unable to reload configuration", e);
source.sendMessage(Component.text( source.sendMessage(Identity.nil(), Component.text(
"Unable to reload your configuration. Check the console for more details.", "Unable to reload your configuration. Check the console for more details.",
NamedTextColor.RED)); NamedTextColor.RED));
} }
@ -174,7 +176,7 @@ public class VelocityCommand implements SimpleCommand {
@Override @Override
public void execute(CommandSource source, String @NonNull [] args) { public void execute(CommandSource source, String @NonNull [] args) {
if (args.length != 0) { if (args.length != 0) {
source.sendMessage(Component.text("/velocity version", NamedTextColor.RED)); source.sendMessage(Identity.nil(), Component.text("/velocity version", NamedTextColor.RED));
return; return;
} }
@ -188,8 +190,8 @@ public class VelocityCommand implements SimpleCommand {
TextComponent copyright = TextComponent TextComponent copyright = TextComponent
.of("Copyright 2018-2020 " + version.getVendor() + ". " + version.getName() .of("Copyright 2018-2020 " + version.getVendor() + ". " + version.getName()
+ " is freely licensed under the terms of the MIT License."); + " is freely licensed under the terms of the MIT License.");
source.sendMessage(velocity); source.sendMessage(Identity.nil(), velocity);
source.sendMessage(copyright); source.sendMessage(Identity.nil(), copyright);
if (version.getName().equals("Velocity")) { if (version.getName().equals("Velocity")) {
TextComponent velocityWebsite = Component.text() TextComponent velocityWebsite = Component.text()
@ -206,7 +208,7 @@ public class VelocityCommand implements SimpleCommand {
"https://github.com/VelocityPowered/Velocity")) "https://github.com/VelocityPowered/Velocity"))
.build()) .build())
.build(); .build();
source.sendMessage(velocityWebsite); source.sendMessage(Identity.nil(), velocityWebsite);
} }
} }
@ -227,7 +229,7 @@ public class VelocityCommand implements SimpleCommand {
@Override @Override
public void execute(CommandSource source, String @NonNull [] args) { public void execute(CommandSource source, String @NonNull [] args) {
if (args.length != 0) { if (args.length != 0) {
source.sendMessage(Component.text("/velocity plugins", NamedTextColor.RED)); source.sendMessage(Identity.nil(), Component.text("/velocity plugins", NamedTextColor.RED));
return; return;
} }
@ -235,7 +237,8 @@ public class VelocityCommand implements SimpleCommand {
int pluginCount = plugins.size(); int pluginCount = plugins.size();
if (pluginCount == 0) { if (pluginCount == 0) {
source.sendMessage(Component.text("No plugins installed.", NamedTextColor.YELLOW)); source.sendMessage(Identity.nil(), Component.text(
"No plugins installed.", NamedTextColor.YELLOW));
return; return;
} }
@ -249,7 +252,7 @@ public class VelocityCommand implements SimpleCommand {
} }
} }
source.sendMessage(output.build()); source.sendMessage(Identity.nil(), output.build());
} }
private TextComponent componentForPlugin(PluginDescription description) { private TextComponent componentForPlugin(PluginDescription description) {

Datei anzeigen

@ -18,6 +18,7 @@ import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled; import io.netty.buffer.Unpooled;
import java.util.Optional; import java.util.Optional;
import java.util.StringJoiner; import java.util.StringJoiner;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.ComponentSerializer; import net.kyori.adventure.text.serializer.ComponentSerializer;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
@ -155,9 +156,9 @@ class BungeeCordMessageResponder {
Component messageComponent = serializer.deserialize(message); Component messageComponent = serializer.deserialize(message);
if (target.equals("ALL")) { if (target.equals("ALL")) {
proxy.sendMessage(messageComponent); proxy.sendMessage(Identity.nil(), messageComponent);
} else { } else {
proxy.getPlayer(target).ifPresent(player -> player.sendMessage(messageComponent)); proxy.getPlayer(target).ifPresent(player -> player.sendMessage(Identity.nil(), messageComponent));
} }
} }

Datei anzeigen

@ -43,6 +43,7 @@ import java.util.Optional;
import java.util.Queue; import java.util.Queue;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
@ -137,7 +138,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
.exceptionally(e -> { .exceptionally(e -> {
logger.info("Exception occurred while running command for {}", logger.info("Exception occurred while running command for {}",
player.getUsername(), e); player.getUsername(), e);
player.sendMessage( player.sendMessage(Identity.nil(),
Component.text("An error occurred while running this command.", Component.text("An error occurred while running this command.",
NamedTextColor.RED)); NamedTextColor.RED));
return null; return null;

Datei anzeigen

@ -68,8 +68,8 @@ import java.util.concurrent.CompletionException;
import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.ThreadLocalRandom;
import net.kyori.adventure.audience.MessageType; import net.kyori.adventure.audience.MessageType;
import net.kyori.adventure.bossbar.BossBar; import net.kyori.adventure.bossbar.BossBar;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.TranslatableComponent; import net.kyori.adventure.text.TranslatableComponent;
import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
@ -90,6 +90,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
private static final Logger logger = LogManager.getLogger(ConnectedPlayer.class); private static final Logger logger = LogManager.getLogger(ConnectedPlayer.class);
private final Identity identity = new IdentityImpl();
/** /**
* The actual Minecraft connection. This is actually a wrapper object around the Netty channel. * The actual Minecraft connection. This is actually a wrapper object around the Netty channel.
*/ */
@ -128,6 +129,11 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
this.onlineMode = onlineMode; this.onlineMode = onlineMode;
} }
@Override
public @NonNull Identity identity() {
return this.identity;
}
@Override @Override
public String getUsername() { public String getUsername() {
return profile.getName(); return profile.getName();
@ -258,16 +264,17 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
} }
@Override @Override
public void sendMessage(net.kyori.adventure.text.@NonNull Component message) { public void sendMessage(@NonNull Identity identity, @NonNull Component message) {
connection.write(Chat.createClientbound(message, this.getProtocolVersion())); connection.write(Chat.createClientbound(identity, message, this.getProtocolVersion()));
} }
@Override @Override
public void sendMessage(@NonNull Component message, @NonNull MessageType type) { public void sendMessage(@NonNull Identity identity, @NonNull Component message,
@NonNull MessageType type) {
Preconditions.checkNotNull(message, "message"); Preconditions.checkNotNull(message, "message");
Preconditions.checkNotNull(type, "type"); Preconditions.checkNotNull(type, "type");
Chat packet = Chat.createClientbound(message, this.getProtocolVersion()); Chat packet = Chat.createClientbound(identity, message, this.getProtocolVersion());
packet.setType(type == MessageType.CHAT ? Chat.CHAT_TYPE : Chat.SYSTEM_TYPE); packet.setType(type == MessageType.CHAT ? Chat.CHAT_TYPE : Chat.SYSTEM_TYPE);
connection.write(packet); connection.write(packet);
} }
@ -871,6 +878,13 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player {
return minecraftOrFmlMessage || knownChannels.contains(message.getChannel()); return minecraftOrFmlMessage || knownChannels.contains(message.getChannel());
} }
private class IdentityImpl implements Identity {
@Override
public @NonNull UUID uuid() {
return ConnectedPlayer.this.getUniqueId();
}
}
private class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder { private class ConnectionRequestBuilderImpl implements ConnectionRequestBuilder {
private final RegisteredServer toConnect; private final RegisteredServer toConnect;

Datei anzeigen

@ -8,6 +8,8 @@ import com.velocitypowered.api.permission.Tristate;
import com.velocitypowered.api.proxy.ConsoleCommandSource; import com.velocitypowered.api.proxy.ConsoleCommandSource;
import com.velocitypowered.proxy.VelocityServer; import com.velocitypowered.proxy.VelocityServer;
import java.util.List; import java.util.List;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.text.Component;
import net.kyori.text.TextComponent; import net.kyori.text.TextComponent;
import net.kyori.text.format.TextColor; import net.kyori.text.format.TextColor;
import net.minecrell.terminalconsole.SimpleTerminalConsole; import net.minecrell.terminalconsole.SimpleTerminalConsole;
@ -38,7 +40,7 @@ public final class VelocityConsole extends SimpleTerminalConsole implements Cons
} }
@Override @Override
public void sendMessage(net.kyori.adventure.text.@NonNull Component message) { public void sendMessage(@NonNull Identity identity, @NonNull Component message) {
logger.info(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection() logger.info(net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection()
.serialize(message)); .serialize(message));
} }

Datei anzeigen

@ -6,6 +6,7 @@ import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
import com.velocitypowered.proxy.protocol.MinecraftPacket; import com.velocitypowered.proxy.protocol.MinecraftPacket;
import com.velocitypowered.proxy.protocol.ProtocolUtils; import com.velocitypowered.proxy.protocol.ProtocolUtils;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import net.kyori.adventure.identity.Identity;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import java.util.UUID; import java.util.UUID;
@ -109,9 +110,9 @@ public class Chat implements MinecraftPacket {
.serialize(component), type, sender); .serialize(component), type, sender);
} }
public static Chat createClientbound(net.kyori.adventure.text.Component component, public static Chat createClientbound(Identity identity,
ProtocolVersion version) { net.kyori.adventure.text.Component component, ProtocolVersion version) {
return createClientbound(component, CHAT_TYPE, EMPTY_SENDER, version); return createClientbound(component, CHAT_TYPE, identity.uuid(), version);
} }
public static Chat createClientbound(net.kyori.adventure.text.Component component, byte type, public static Chat createClientbound(net.kyori.adventure.text.Component component, byte type,