geforkt von Mirrors/Velocity
Merge remote-tracking branch 'refs/remotes/upstream/dev/3.0.0'
Dieser Commit ist enthalten in:
Commit
6e33bc6c17
69
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normale Datei
69
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
Normale Datei
@ -0,0 +1,69 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Report issues with Velocity not working properly.
|
||||||
|
labels: ["type: bug"]
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: What you expected to work and how.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Actual Behavior
|
||||||
|
description: What actually happens.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Steps to Reproduce
|
||||||
|
description: Information on how we can reproduce this bug on our own, this can be e.g. just an explanation, a video or your Velocity config.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Plugin List
|
||||||
|
description: |
|
||||||
|
All plugins running on your proxy and the backend server you're experiencing this issue on.
|
||||||
|
Use `/velocity plugins` to list plugins on Velocity and `/plugins` to list plugins on your backend server.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Velocity Version
|
||||||
|
description: |
|
||||||
|
The full, unmodified output of running `/velocity info`.
|
||||||
|
*"Latest"* is not a version. We require you to paste the text, not a screenshot.
|
||||||
|
<details>
|
||||||
|
<summary>Example</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
[17:44:10 INFO]: Velocity 3.3.0-SNAPSHOT (git-9d25d309-b400)
|
||||||
|
[17:44:10 INFO]: Copyright 2018-2023 Velocity Contributors. Velocity is licensed under the terms of the GNU General Public License v3.
|
||||||
|
[17:44:10 INFO]: velocitypowered.com - GitHub
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional Information
|
||||||
|
description: Anything else you think is helpful.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Before submitting this issue, please ensure the following:
|
||||||
|
|
||||||
|
1. You are running the latest version of Velocity from [our downloads page](https://papermc.io/downloads/velocity).
|
||||||
|
2. You searched for and ensured there isn't already an open issue regarding this.
|
||||||
|
|
||||||
|
If you think you have a bug, but are not sure, feel free to ask in the `#velocity-help` channel on our
|
||||||
|
[Discord](https://discord.gg/papermc).
|
10
.github/ISSUE_TEMPLATE/config.yml
vendored
Normale Datei
10
.github/ISSUE_TEMPLATE/config.yml
vendored
Normale Datei
@ -0,0 +1,10 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: PaperMC Discord
|
||||||
|
url: https://discord.gg/papermc
|
||||||
|
about: If you are having issues with the proxy not connecting to servers or have other minor issues, come ask us on our Discord server!
|
||||||
|
- name: Exploit Report
|
||||||
|
url: https://discord.gg/papermc
|
||||||
|
about: |
|
||||||
|
Due to GitHub not currently allowing private issues, exploit reports are currently handled via our Discord.
|
||||||
|
To report an exploit, see the #paper-exploit-report channel.
|
48
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normale Datei
48
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normale Datei
@ -0,0 +1,48 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Request for a feature to be implemented into Velocity.
|
||||||
|
labels: ["type: feature"]
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Requested Feature
|
||||||
|
description: |
|
||||||
|
Please describe as best as you can what you'd like to be added to Velocity.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Why is this needed?
|
||||||
|
description: |
|
||||||
|
Please describe why do you need this feature.
|
||||||
|
Do you think it could be useful? Is it due to another problem?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Alternative Solutions
|
||||||
|
description: |
|
||||||
|
Are there any alternative solutions to implementing a new feature?
|
||||||
|
What have you tried instead?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional Information
|
||||||
|
description: Anything else you want to add.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Before submitting this request, please ensure the following:
|
||||||
|
|
||||||
|
1. You are running the latest version of Velocity from [our downloads page](https://papermc.io/downloads/velocity).
|
||||||
|
2. You searched for and ensured there isn't already an open issue regarding this.
|
||||||
|
3. The feature you're requesting has to be implemented on Velocity and not on the backend server.
|
||||||
|
|
||||||
|
If you are unsure whether your problem can already be fixed in another way, feel free to ask in the `#velocity-help` channel on our
|
||||||
|
[Discord](https://discord.gg/papermc).
|
@ -67,7 +67,9 @@ tasks {
|
|||||||
"https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/",
|
"https://google.github.io/guice/api-docs/${libs.guice.get().version}/javadoc/",
|
||||||
"https://docs.oracle.com/en/java/javase/17/docs/api/",
|
"https://docs.oracle.com/en/java/javase/17/docs/api/",
|
||||||
"https://jd.advntr.dev/api/${libs.adventure.bom.get().version}/",
|
"https://jd.advntr.dev/api/${libs.adventure.bom.get().version}/",
|
||||||
"https://javadoc.io/doc/com.github.ben-manes.caffeine/caffeine"
|
"https://jd.advntr.dev/text-minimessage/${libs.adventure.bom.get().version}/",
|
||||||
|
"https://jd.advntr.dev/key/${libs.adventure.bom.get().version}/",
|
||||||
|
"https://javadoc.io/doc/com.github.ben-manes.caffeine/caffeine/${libs.caffeine.get().version}/",
|
||||||
)
|
)
|
||||||
|
|
||||||
o.tags(
|
o.tags(
|
||||||
|
@ -44,7 +44,7 @@ public interface CommandManager {
|
|||||||
* @param otherAliases additional aliases
|
* @param otherAliases additional aliases
|
||||||
* @throws IllegalArgumentException if one of the given aliases is already registered, or
|
* @throws IllegalArgumentException if one of the given aliases is already registered, or
|
||||||
* the given command does not implement a registrable {@link Command} subinterface
|
* the given command does not implement a registrable {@link Command} subinterface
|
||||||
* @see Command for a list of registrable {@link Command} subinterfaces
|
* @see Command for a list of registrable Command subinterfaces
|
||||||
*/
|
*/
|
||||||
default void register(String alias, Command command, String... otherAliases) {
|
default void register(String alias, Command command, String... otherAliases) {
|
||||||
register(metaBuilder(alias).aliases(otherAliases).build(), command);
|
register(metaBuilder(alias).aliases(otherAliases).build(), command);
|
||||||
@ -65,7 +65,7 @@ public interface CommandManager {
|
|||||||
* @param command the command to register
|
* @param command the command to register
|
||||||
* @throws IllegalArgumentException if one of the given aliases is already registered, or
|
* @throws IllegalArgumentException if one of the given aliases is already registered, or
|
||||||
* the given command does not implement a registrable {@link Command} subinterface
|
* the given command does not implement a registrable {@link Command} subinterface
|
||||||
* @see Command for a list of registrable {@link Command} subinterfaces
|
* @see Command for a list of registrable Command subinterfaces
|
||||||
*/
|
*/
|
||||||
void register(CommandMeta meta, Command command);
|
void register(CommandMeta meta, Command command);
|
||||||
|
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2018-2023 Velocity Contributors
|
||||||
|
*
|
||||||
|
* The Velocity API is licensed under the terms of the MIT License. For more details,
|
||||||
|
* reference the LICENSE file in the api top-level directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.api.event.player;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.velocitypowered.api.event.ResultedEvent;
|
||||||
|
import com.velocitypowered.api.event.annotation.AwaitingEvent;
|
||||||
|
import com.velocitypowered.api.proxy.ServerConnection;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event is fired when the downstream server tries to remove a resource pack from player
|
||||||
|
* or clear all of them. The proxy will wait on this event to finish before forwarding the
|
||||||
|
* action to the user. If this event is denied, no resource packs will be removed from player.
|
||||||
|
*/
|
||||||
|
@AwaitingEvent
|
||||||
|
public class ServerResourcePackRemoveEvent implements ResultedEvent<ResultedEvent.GenericResult> {
|
||||||
|
|
||||||
|
private GenericResult result;
|
||||||
|
private final @MonotonicNonNull UUID packId;
|
||||||
|
private final ServerConnection serverConnection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiates this event.
|
||||||
|
*/
|
||||||
|
public ServerResourcePackRemoveEvent(UUID packId, ServerConnection serverConnection) {
|
||||||
|
this.result = ResultedEvent.GenericResult.allowed();
|
||||||
|
this.packId = packId;
|
||||||
|
this.serverConnection = serverConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the id of the resource pack, if it's null all the resource packs
|
||||||
|
* from player will be cleared.
|
||||||
|
*
|
||||||
|
* @return the id
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public UUID getPackId() {
|
||||||
|
return packId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the server that tries to remove a resource pack from player or clear all of them.
|
||||||
|
*
|
||||||
|
* @return the server connection
|
||||||
|
*/
|
||||||
|
public ServerConnection getServerConnection() {
|
||||||
|
return serverConnection;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GenericResult getResult() {
|
||||||
|
return this.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setResult(GenericResult result) {
|
||||||
|
this.result = Preconditions.checkNotNull(result, "result");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Velocity Contributors
|
||||||
|
*
|
||||||
|
* The Velocity API is licensed under the terms of the MIT License. For more details,
|
||||||
|
* reference the LICENSE file in the api top-level directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.api.event.player.configuration;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.event.annotation.AwaitingEvent;
|
||||||
|
import com.velocitypowered.api.proxy.Player;
|
||||||
|
import com.velocitypowered.api.proxy.ServerConnection;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event is executed when a player entered the configuration state and can be configured by Velocity.
|
||||||
|
* <p>Velocity will wait for this event before continuing/ending the configuration state.</p>
|
||||||
|
*
|
||||||
|
* @param player The player who can be configured.
|
||||||
|
* @param server The server that is currently configuring the player.
|
||||||
|
* @since 3.3.0
|
||||||
|
* @sinceMinecraft 1.20.2
|
||||||
|
*/
|
||||||
|
@AwaitingEvent
|
||||||
|
public record PlayerConfigurationEvent(@NotNull Player player, ServerConnection server) {
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Velocity Contributors
|
||||||
|
*
|
||||||
|
* The Velocity API is licensed under the terms of the MIT License. For more details,
|
||||||
|
* reference the LICENSE file in the api top-level directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.api.event.player.configuration;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.event.annotation.AwaitingEvent;
|
||||||
|
import com.velocitypowered.api.proxy.Player;
|
||||||
|
import com.velocitypowered.api.proxy.ServerConnection;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event is executed when a player is about to enter the configuration state.
|
||||||
|
* It is <b>not</b> called for the initial configuration of a player after login.
|
||||||
|
* <p>Velocity will wait for this event before asking the client to enter configuration state.
|
||||||
|
* However due to backend server being unable to keep the connection alive during state changes,
|
||||||
|
* Velocity will only wait for a maximum of 5 seconds.</p>
|
||||||
|
*
|
||||||
|
* @param player The player who is about to enter configuration state.
|
||||||
|
* @param server The server that wants to reconfigure the player.
|
||||||
|
* @since 3.3.0
|
||||||
|
* @sinceMinecraft 1.20.2
|
||||||
|
*/
|
||||||
|
@AwaitingEvent
|
||||||
|
public record PlayerEnterConfigurationEvent(@NotNull Player player, ServerConnection server) {
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Velocity Contributors
|
||||||
|
*
|
||||||
|
* The Velocity API is licensed under the terms of the MIT License. For more details,
|
||||||
|
* reference the LICENSE file in the api top-level directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.api.event.player.configuration;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.network.ProtocolState;
|
||||||
|
import com.velocitypowered.api.proxy.Player;
|
||||||
|
import com.velocitypowered.api.proxy.ServerConnection;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event is executed when a player has entered the configuration state.
|
||||||
|
* <p>From this moment on, until the {@link PlayerFinishedConfigurationEvent} is executed,
|
||||||
|
* the {@linkplain Player#getProtocolState()} method is guaranteed
|
||||||
|
* to return {@link ProtocolState#CONFIGURATION}.</p>
|
||||||
|
*
|
||||||
|
* @param player The player who has entered the configuration state.
|
||||||
|
* @param server The server that will now (re-)configure the player.
|
||||||
|
* @since 3.3.0
|
||||||
|
* @sinceMinecraft 1.20.2
|
||||||
|
*/
|
||||||
|
public record PlayerEnteredConfigurationEvent(@NotNull Player player, ServerConnection server) {
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Velocity Contributors
|
||||||
|
*
|
||||||
|
* The Velocity API is licensed under the terms of the MIT License. For more details,
|
||||||
|
* reference the LICENSE file in the api top-level directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.api.event.player.configuration;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.event.annotation.AwaitingEvent;
|
||||||
|
import com.velocitypowered.api.proxy.Player;
|
||||||
|
import com.velocitypowered.api.proxy.ServerConnection;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event is executed when a player is about to finish the configuration state.
|
||||||
|
* <p>Velocity will wait for this event before asking the client to finish the configuration state.
|
||||||
|
* However due to backend server being unable to keep the connection alive during state changes,
|
||||||
|
* Velocity will only wait for a maximum of 5 seconds. If you need to hold a player in configuration
|
||||||
|
* state, use the {@link PlayerConfigurationEvent}.</p>
|
||||||
|
*
|
||||||
|
* @param player The player who is about to finish the configuration phase.
|
||||||
|
* @param server The server that has (re-)configured the player.
|
||||||
|
* @since 3.3.0
|
||||||
|
* @sinceMinecraft 1.20.2
|
||||||
|
*/
|
||||||
|
@AwaitingEvent
|
||||||
|
public record PlayerFinishConfigurationEvent(@NotNull Player player, @NotNull ServerConnection server) {
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Velocity Contributors
|
||||||
|
*
|
||||||
|
* The Velocity API is licensed under the terms of the MIT License. For more details,
|
||||||
|
* reference the LICENSE file in the api top-level directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.api.event.player.configuration;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.network.ProtocolState;
|
||||||
|
import com.velocitypowered.api.proxy.Player;
|
||||||
|
import com.velocitypowered.api.proxy.ServerConnection;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event is executed when a player has finished the configuration state.
|
||||||
|
* <p>From this moment on, the {@link Player#getProtocolState()} method
|
||||||
|
* will return {@link ProtocolState#PLAY}.</p>
|
||||||
|
*
|
||||||
|
* @param player The player who has finished the configuration state.
|
||||||
|
* @param server The server that has (re-)configured the player.
|
||||||
|
* @since 3.3.0
|
||||||
|
* @sinceMinecraft 1.20.2
|
||||||
|
*/
|
||||||
|
public record PlayerFinishedConfigurationEvent(@NotNull Player player, @NotNull ServerConnection server) {
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Velocity Contributors
|
||||||
|
*
|
||||||
|
* The Velocity API is licensed under the terms of the MIT License. For more details,
|
||||||
|
* reference the LICENSE file in the api top-level directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.api.event.proxy.server;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
|
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event is fired by the proxy after a backend server is registered to the server map.
|
||||||
|
* Currently, it may occur when a server is registered dynamically at runtime or when a server is
|
||||||
|
* replaced due to configuration reload.
|
||||||
|
*
|
||||||
|
* @see com.velocitypowered.api.proxy.ProxyServer#registerServer(ServerInfo)
|
||||||
|
*
|
||||||
|
* @param registeredServer A {@link RegisteredServer} that has been registered.
|
||||||
|
* @since 3.3.0
|
||||||
|
*/
|
||||||
|
@Beta
|
||||||
|
public record ServerRegisteredEvent(@NotNull RegisteredServer registeredServer) {
|
||||||
|
public ServerRegisteredEvent {
|
||||||
|
Preconditions.checkNotNull(registeredServer, "registeredServer");
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Velocity Contributors
|
||||||
|
*
|
||||||
|
* The Velocity API is licensed under the terms of the MIT License. For more details,
|
||||||
|
* reference the LICENSE file in the api top-level directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.api.event.proxy.server;
|
||||||
|
|
||||||
|
import com.google.common.annotations.Beta;
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
|
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This event is fired by the proxy after a backend server is unregistered from the server map.
|
||||||
|
* Currently, it may occur when a server is unregistered dynamically at runtime
|
||||||
|
* or when a server is replaced due to configuration reload.
|
||||||
|
*
|
||||||
|
* @see com.velocitypowered.api.proxy.ProxyServer#unregisterServer(ServerInfo)
|
||||||
|
*
|
||||||
|
* @param unregisteredServer A {@link RegisteredServer} that has been unregistered.
|
||||||
|
* @since 3.3.0
|
||||||
|
*/
|
||||||
|
@Beta
|
||||||
|
public record ServerUnregisteredEvent(@NotNull RegisteredServer unregisteredServer) {
|
||||||
|
public ServerUnregisteredEvent {
|
||||||
|
Preconditions.checkNotNull(unregisteredServer, "unregisteredServer");
|
||||||
|
}
|
||||||
|
}
|
@ -87,7 +87,7 @@ public enum ProtocolVersion implements Ordered<ProtocolVersion> {
|
|||||||
MINECRAFT_1_20_2(764, "1.20.2"),
|
MINECRAFT_1_20_2(764, "1.20.2"),
|
||||||
MINECRAFT_1_20_3(765, "1.20.3", "1.20.4"),
|
MINECRAFT_1_20_3(765, "1.20.3", "1.20.4"),
|
||||||
MINECRAFT_1_20_5(766, "1.20.5", "1.20.6"),
|
MINECRAFT_1_20_5(766, "1.20.5", "1.20.6"),
|
||||||
MINECRAFT_1_21(767, "1.21");
|
MINECRAFT_1_21(767, "1.21", "1.21.1");
|
||||||
|
|
||||||
private static final int SNAPSHOT_BIT = 30;
|
private static final int SNAPSHOT_BIT = 30;
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ import com.velocitypowered.api.proxy.player.TabList;
|
|||||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
import com.velocitypowered.api.util.GameProfile;
|
import com.velocitypowered.api.util.GameProfile;
|
||||||
import com.velocitypowered.api.util.ModInfo;
|
import com.velocitypowered.api.util.ModInfo;
|
||||||
|
import com.velocitypowered.api.util.ServerLink;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -461,4 +462,16 @@ public interface Player extends
|
|||||||
* @sinceMinecraft 1.20.5
|
* @sinceMinecraft 1.20.5
|
||||||
*/
|
*/
|
||||||
void requestCookie(Key key);
|
void requestCookie(Key key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the player a list of custom links to display in their client's pause menu.
|
||||||
|
*
|
||||||
|
* <p>Note that later packets sent by the backend server may override links sent by the proxy.
|
||||||
|
*
|
||||||
|
* @param links an ordered list of {@link ServerLink}s to send to the player
|
||||||
|
* @throws IllegalArgumentException if the player is from a version lower than 1.21
|
||||||
|
* @since 3.3.0
|
||||||
|
* @sinceMinecraft 1.21
|
||||||
|
*/
|
||||||
|
void setServerLinks(@NotNull List<ServerLink> links);
|
||||||
}
|
}
|
101
api/src/main/java/com/velocitypowered/api/util/ServerLink.java
Normale Datei
101
api/src/main/java/com/velocitypowered/api/util/ServerLink.java
Normale Datei
@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2021-2024 Velocity Contributors
|
||||||
|
*
|
||||||
|
* The Velocity API is licensed under the terms of the MIT License. For more details,
|
||||||
|
* reference the LICENSE file in the api top-level directory.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.api.util;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Optional;
|
||||||
|
import net.kyori.adventure.text.Component;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a custom URL servers can show in player pause menus.
|
||||||
|
* Links can be of a built-in type or use a custom component text label.
|
||||||
|
*/
|
||||||
|
public final class ServerLink {
|
||||||
|
|
||||||
|
private @Nullable Type type;
|
||||||
|
private @Nullable Component label;
|
||||||
|
private final URI url;
|
||||||
|
|
||||||
|
private ServerLink(Component label, String url) {
|
||||||
|
this.label = Preconditions.checkNotNull(label, "label");
|
||||||
|
this.url = URI.create(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ServerLink(Type type, String url) {
|
||||||
|
this.type = Preconditions.checkNotNull(type, "type");
|
||||||
|
this.url = URI.create(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a server link with a custom component label.
|
||||||
|
*
|
||||||
|
* @param label a custom component label to display
|
||||||
|
* @param link the URL to open when clicked
|
||||||
|
*/
|
||||||
|
public static ServerLink serverLink(Component label, String link) {
|
||||||
|
return new ServerLink(label, link);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a server link with a built-in type.
|
||||||
|
*
|
||||||
|
* @param type the {@link Type built-in type} of link
|
||||||
|
* @param link the URL to open when clicked
|
||||||
|
*/
|
||||||
|
public static ServerLink serverLink(Type type, String link) {
|
||||||
|
return new ServerLink(type, link);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the type of the server link.
|
||||||
|
*
|
||||||
|
* @return the type of the server link
|
||||||
|
*/
|
||||||
|
public Optional<Type> getBuiltInType() {
|
||||||
|
return Optional.ofNullable(type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the custom component label of the server link.
|
||||||
|
*
|
||||||
|
* @return the custom component label of the server link
|
||||||
|
*/
|
||||||
|
public Optional<Component> getCustomLabel() {
|
||||||
|
return Optional.ofNullable(label);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the link {@link URI}.
|
||||||
|
*
|
||||||
|
* @return the link {@link URI}
|
||||||
|
*/
|
||||||
|
public URI getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Built-in types of server links.
|
||||||
|
*
|
||||||
|
* @apiNote {@link Type#BUG_REPORT} links are shown on the connection error screen
|
||||||
|
*/
|
||||||
|
public enum Type {
|
||||||
|
BUG_REPORT,
|
||||||
|
COMMUNITY_GUIDELINES,
|
||||||
|
SUPPORT,
|
||||||
|
STATUS,
|
||||||
|
FEEDBACK,
|
||||||
|
COMMUNITY,
|
||||||
|
WEBSITE,
|
||||||
|
FORUMS,
|
||||||
|
NEWS,
|
||||||
|
ANNOUNCEMENTS
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -11,7 +11,7 @@ shadow = "io.github.goooler.shadow:8.1.5"
|
|||||||
spotless = "com.diffplug.spotless:6.25.0"
|
spotless = "com.diffplug.spotless:6.25.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
adventure-bom = "net.kyori:adventure-bom:4.16.0"
|
adventure-bom = "net.kyori:adventure-bom:4.17.0"
|
||||||
adventure-facet = "net.kyori:adventure-platform-facet:4.3.2"
|
adventure-facet = "net.kyori:adventure-platform-facet:4.3.2"
|
||||||
asm = "org.ow2.asm:asm:9.6"
|
asm = "org.ow2.asm:asm:9.6"
|
||||||
auto-service = "com.google.auto.service:auto-service:1.0.1"
|
auto-service = "com.google.auto.service:auto-service:1.0.1"
|
||||||
|
@ -45,7 +45,7 @@ import com.velocitypowered.proxy.command.builtin.ShutdownCommand;
|
|||||||
import com.velocitypowered.proxy.command.builtin.VelocityCommand;
|
import com.velocitypowered.proxy.command.builtin.VelocityCommand;
|
||||||
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
import com.velocitypowered.proxy.config.VelocityConfiguration;
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo;
|
import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo;
|
||||||
import com.velocitypowered.proxy.connection.util.ServerListPingHandler;
|
import com.velocitypowered.proxy.connection.util.ServerListPingHandler;
|
||||||
import com.velocitypowered.proxy.console.VelocityConsole;
|
import com.velocitypowered.proxy.console.VelocityConsole;
|
||||||
import com.velocitypowered.proxy.crypto.EncryptionUtils;
|
import com.velocitypowered.proxy.crypto.EncryptionUtils;
|
||||||
|
@ -379,7 +379,7 @@ public final class VelocityCommand {
|
|||||||
this.heapGenerator.invoke(hotspotMbean, file.toString(), true);
|
this.heapGenerator.invoke(hotspotMbean, file.toString(), true);
|
||||||
} catch (Throwable e1) {
|
} catch (Throwable e1) {
|
||||||
// This should not occur
|
// This should not occur
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e1);
|
||||||
}
|
}
|
||||||
src.sendMessage(Component.text("Heap dump saved to " + file, NamedTextColor.GREEN));
|
src.sendMessage(Component.text("Heap dump saved to " + file, NamedTextColor.GREEN));
|
||||||
};
|
};
|
||||||
|
@ -60,7 +60,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
private static final Logger logger = LogManager.getLogger(VelocityConfiguration.class);
|
private static final Logger logger = LogManager.getLogger(VelocityConfiguration.class);
|
||||||
|
|
||||||
@Expose
|
@Expose
|
||||||
private String bind = "0.0.0.0:25577";
|
private String bind = "0.0.0.0:25565";
|
||||||
@Expose
|
@Expose
|
||||||
private String motd = "<aqua>A Velocity Server";
|
private String motd = "<aqua>A Velocity Server";
|
||||||
@Expose
|
@Expose
|
||||||
@ -503,7 +503,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough",
|
final PingPassthroughMode pingPassthroughMode = config.getEnumOrElse("ping-passthrough",
|
||||||
PingPassthroughMode.DISABLED);
|
PingPassthroughMode.DISABLED);
|
||||||
|
|
||||||
final String bind = config.getOrElse("bind", "0.0.0.0:25577");
|
final String bind = config.getOrElse("bind", "0.0.0.0:25565");
|
||||||
final int maxPlayers = config.getIntOrElse("show-max-players", 500);
|
final int maxPlayers = config.getIntOrElse("show-max-players", 500);
|
||||||
final boolean onlineMode = config.getOrElse("online-mode", true);
|
final boolean onlineMode = config.getOrElse("online-mode", true);
|
||||||
final boolean forceKeyAuthentication = config.getOrElse("force-key-authentication", true);
|
final boolean forceKeyAuthentication = config.getOrElse("force-key-authentication", true);
|
||||||
@ -830,7 +830,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
@Expose
|
@Expose
|
||||||
private boolean queryEnabled = false;
|
private boolean queryEnabled = false;
|
||||||
@Expose
|
@Expose
|
||||||
private int queryPort = 25577;
|
private int queryPort = 25565;
|
||||||
@Expose
|
@Expose
|
||||||
private String queryMap = "Velocity";
|
private String queryMap = "Velocity";
|
||||||
@Expose
|
@Expose
|
||||||
@ -849,7 +849,7 @@ public class VelocityConfiguration implements ProxyConfig {
|
|||||||
private Query(CommentedConfig config) {
|
private Query(CommentedConfig config) {
|
||||||
if (config != null) {
|
if (config != null) {
|
||||||
this.queryEnabled = config.getOrElse("enabled", false);
|
this.queryEnabled = config.getOrElse("enabled", false);
|
||||||
this.queryPort = config.getIntOrElse("port", 25577);
|
this.queryPort = config.getIntOrElse("port", 25565);
|
||||||
this.queryMap = config.getOrElse("map", "Velocity");
|
this.queryMap = config.getOrElse("map", "Velocity");
|
||||||
this.showPlugins = config.getOrElse("show-plugins", false);
|
this.showPlugins = config.getOrElse("show-plugins", false);
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,8 @@ import com.velocitypowered.proxy.protocol.netty.MinecraftCompressorAndLengthEnco
|
|||||||
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
|
import com.velocitypowered.proxy.protocol.netty.MinecraftDecoder;
|
||||||
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
|
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
|
||||||
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder;
|
import com.velocitypowered.proxy.protocol.netty.MinecraftVarintLengthEncoder;
|
||||||
import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueHandler;
|
import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueInboundHandler;
|
||||||
|
import com.velocitypowered.proxy.protocol.netty.PlayPacketQueueOutboundHandler;
|
||||||
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
|
import com.velocitypowered.proxy.protocol.packet.SetCompressionPacket;
|
||||||
import com.velocitypowered.proxy.util.except.QuietDecoderException;
|
import com.velocitypowered.proxy.util.except.QuietDecoderException;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
@ -148,13 +149,11 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg instanceof MinecraftPacket) {
|
if (msg instanceof MinecraftPacket pkt) {
|
||||||
MinecraftPacket pkt = (MinecraftPacket) msg;
|
|
||||||
if (!pkt.handle(activeSessionHandler)) {
|
if (!pkt.handle(activeSessionHandler)) {
|
||||||
activeSessionHandler.handleGeneric((MinecraftPacket) msg);
|
activeSessionHandler.handleGeneric((MinecraftPacket) msg);
|
||||||
}
|
}
|
||||||
} else if (msg instanceof HAProxyMessage) {
|
} else if (msg instanceof HAProxyMessage proxyMessage) {
|
||||||
HAProxyMessage proxyMessage = (HAProxyMessage) msg;
|
|
||||||
this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(),
|
this.remoteAddress = new InetSocketAddress(proxyMessage.sourceAddress(),
|
||||||
proxyMessage.sourcePort());
|
proxyMessage.sourcePort());
|
||||||
} else if (msg instanceof ByteBuf) {
|
} else if (msg instanceof ByteBuf) {
|
||||||
@ -383,9 +382,14 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
if (state == StateRegistry.CONFIG) {
|
if (state == StateRegistry.CONFIG) {
|
||||||
// Activate the play packet queue
|
// Activate the play packet queue
|
||||||
addPlayPacketQueueHandler();
|
addPlayPacketQueueHandler();
|
||||||
} else if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE) != null) {
|
} else {
|
||||||
// Remove the queue
|
// Remove the queue
|
||||||
this.channel.pipeline().remove(Connections.PLAY_PACKET_QUEUE);
|
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) != null) {
|
||||||
|
this.channel.pipeline().remove(Connections.PLAY_PACKET_QUEUE_OUTBOUND);
|
||||||
|
}
|
||||||
|
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_INBOUND) != null) {
|
||||||
|
this.channel.pipeline().remove(Connections.PLAY_PACKET_QUEUE_INBOUND);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -393,10 +397,13 @@ public class MinecraftConnection extends ChannelInboundHandlerAdapter {
|
|||||||
* Adds the play packet queue handler.
|
* Adds the play packet queue handler.
|
||||||
*/
|
*/
|
||||||
public void addPlayPacketQueueHandler() {
|
public void addPlayPacketQueueHandler() {
|
||||||
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE) == null) {
|
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_OUTBOUND) == null) {
|
||||||
this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE,
|
this.channel.pipeline().addAfter(Connections.MINECRAFT_ENCODER, Connections.PLAY_PACKET_QUEUE_OUTBOUND,
|
||||||
new PlayPacketQueueHandler(this.protocolVersion,
|
new PlayPacketQueueOutboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftEncoder.class).getDirection()));
|
||||||
channel.pipeline().get(MinecraftEncoder.class).getDirection()));
|
}
|
||||||
|
if (this.channel.pipeline().get(Connections.PLAY_PACKET_QUEUE_INBOUND) == null) {
|
||||||
|
this.channel.pipeline().addAfter(Connections.MINECRAFT_DECODER, Connections.PLAY_PACKET_QUEUE_INBOUND,
|
||||||
|
new PlayPacketQueueInboundHandler(this.protocolVersion, channel.pipeline().get(MinecraftDecoder.class).getDirection()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@ import com.velocitypowered.api.event.connection.PreTransferEvent;
|
|||||||
import com.velocitypowered.api.event.player.CookieRequestEvent;
|
import com.velocitypowered.api.event.player.CookieRequestEvent;
|
||||||
import com.velocitypowered.api.event.player.CookieStoreEvent;
|
import com.velocitypowered.api.event.player.CookieStoreEvent;
|
||||||
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
||||||
|
import com.velocitypowered.api.event.player.ServerResourcePackRemoveEvent;
|
||||||
import com.velocitypowered.api.event.player.ServerResourcePackSendEvent;
|
import com.velocitypowered.api.event.player.ServerResourcePackSendEvent;
|
||||||
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
|
import com.velocitypowered.api.event.proxy.ProxyPingEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
@ -40,8 +41,8 @@ import com.velocitypowered.proxy.connection.MinecraftConnection;
|
|||||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||||
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
|
import com.velocitypowered.proxy.connection.client.ClientPlaySessionHandler;
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo;
|
import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo;
|
||||||
import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackHandler;
|
import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler;
|
||||||
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
|
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
|
||||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||||
@ -258,14 +259,26 @@ public class BackendPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(RemoveResourcePackPacket packet) {
|
public boolean handle(RemoveResourcePackPacket packet) {
|
||||||
final ConnectedPlayer player = serverConn.getPlayer();
|
final ServerResourcePackRemoveEvent event = new ServerResourcePackRemoveEvent(
|
||||||
final ResourcePackHandler handler = player.resourcePackHandler();
|
packet.getId(), this.serverConn);
|
||||||
if (packet.getId() != null) {
|
server.getEventManager().fire(event).thenAcceptAsync(serverResourcePackRemoveEvent -> {
|
||||||
handler.remove(packet.getId());
|
if (playerConnection.isClosed()) {
|
||||||
} else {
|
return;
|
||||||
handler.clearAppliedResourcePacks();
|
}
|
||||||
}
|
if (serverResourcePackRemoveEvent.getResult().isAllowed()) {
|
||||||
playerConnection.write(packet);
|
final ConnectedPlayer player = serverConn.getPlayer();
|
||||||
|
final ResourcePackHandler handler = player.resourcePackHandler();
|
||||||
|
if (packet.getId() != null) {
|
||||||
|
handler.remove(packet.getId());
|
||||||
|
} else {
|
||||||
|
handler.clearAppliedResourcePacks();
|
||||||
|
}
|
||||||
|
playerConnection.write(packet);
|
||||||
|
}
|
||||||
|
}, playerConnection.eventLoop()).exceptionally((ex) -> {
|
||||||
|
logger.error("Exception while handling resource pack remove for {}", playerConnection, ex);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,6 @@ 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;
|
||||||
@ -143,7 +142,7 @@ public class BungeeCordMessageResponder {
|
|||||||
out.writeUTF("PlayerList");
|
out.writeUTF("PlayerList");
|
||||||
out.writeUTF(info.getServerInfo().getName());
|
out.writeUTF(info.getServerInfo().getName());
|
||||||
|
|
||||||
StringJoiner joiner = new StringJoiner(", ");
|
final StringJoiner joiner = new StringJoiner(", ");
|
||||||
for (Player online : info.getPlayersConnected()) {
|
for (Player online : info.getPlayersConnected()) {
|
||||||
joiner.add(online.getUsername());
|
joiner.add(online.getUsername());
|
||||||
}
|
}
|
||||||
@ -187,10 +186,9 @@ public class BungeeCordMessageResponder {
|
|||||||
|
|
||||||
Component messageComponent = serializer.deserialize(message);
|
Component messageComponent = serializer.deserialize(message);
|
||||||
if (target.equals("ALL")) {
|
if (target.equals("ALL")) {
|
||||||
proxy.sendMessage(Identity.nil(), messageComponent);
|
proxy.sendMessage(messageComponent);
|
||||||
} else {
|
} else {
|
||||||
proxy.getPlayer(target).ifPresent(player -> player.sendMessage(Identity.nil(),
|
proxy.getPlayer(target).ifPresent(player -> player.sendMessage(messageComponent));
|
||||||
messageComponent));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,6 +260,13 @@ public class BungeeCordMessageResponder {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void processKickRaw(ByteBufDataInput in) {
|
||||||
|
proxy.getPlayer(in.readUTF()).ifPresent(player -> {
|
||||||
|
String kickReason = in.readUTF();
|
||||||
|
player.disconnect(GsonComponentSerializer.gson().deserialize(kickReason));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void processForwardToPlayer(ByteBufDataInput in) {
|
private void processForwardToPlayer(ByteBufDataInput in) {
|
||||||
Optional<Player> player = proxy.getPlayer(in.readUTF());
|
Optional<Player> player = proxy.getPlayer(in.readUTF());
|
||||||
if (player.isPresent()) {
|
if (player.isPresent()) {
|
||||||
@ -374,6 +379,9 @@ public class BungeeCordMessageResponder {
|
|||||||
case "KickPlayer":
|
case "KickPlayer":
|
||||||
this.processKick(in);
|
this.processKick(in);
|
||||||
break;
|
break;
|
||||||
|
case "KickPlayerRaw":
|
||||||
|
this.processKickRaw(in);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// Do nothing, unknown command
|
// Do nothing, unknown command
|
||||||
break;
|
break;
|
||||||
|
@ -21,6 +21,7 @@ import com.velocitypowered.api.event.connection.PreTransferEvent;
|
|||||||
import com.velocitypowered.api.event.player.CookieRequestEvent;
|
import com.velocitypowered.api.event.player.CookieRequestEvent;
|
||||||
import com.velocitypowered.api.event.player.CookieStoreEvent;
|
import com.velocitypowered.api.event.player.CookieStoreEvent;
|
||||||
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
||||||
|
import com.velocitypowered.api.event.player.ServerResourcePackRemoveEvent;
|
||||||
import com.velocitypowered.api.event.player.ServerResourcePackSendEvent;
|
import com.velocitypowered.api.event.player.ServerResourcePackSendEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
||||||
@ -29,7 +30,8 @@ import com.velocitypowered.proxy.connection.MinecraftConnection;
|
|||||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||||
import com.velocitypowered.proxy.connection.client.ClientConfigSessionHandler;
|
import com.velocitypowered.proxy.connection.client.ClientConfigSessionHandler;
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo;
|
import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo;
|
||||||
|
import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler;
|
||||||
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
|
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
|
||||||
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
|
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults;
|
||||||
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl;
|
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl;
|
||||||
@ -41,6 +43,7 @@ import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
|
import com.velocitypowered.proxy.protocol.packet.DisconnectPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
|
import com.velocitypowered.proxy.protocol.packet.KeepAlivePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
import com.velocitypowered.proxy.protocol.packet.PluginMessagePacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.RemoveResourcePackPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ResourcePackRequestPacket;
|
import com.velocitypowered.proxy.protocol.packet.ResourcePackRequestPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
|
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.TransferPacket;
|
import com.velocitypowered.proxy.protocol.packet.TransferPacket;
|
||||||
@ -132,7 +135,8 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(KeepAlivePacket packet) {
|
public boolean handle(KeepAlivePacket packet) {
|
||||||
serverConn.ensureConnected().write(packet);
|
serverConn.getPendingPings().put(packet.getRandomId(), System.nanoTime());
|
||||||
|
serverConn.getPlayer().getConnection().write(packet);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,31 +196,53 @@ public class ConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(FinishedUpdatePacket packet) {
|
public boolean handle(RemoveResourcePackPacket packet) {
|
||||||
MinecraftConnection smc = serverConn.ensureConnected();
|
final MinecraftConnection playerConnection = this.serverConn.getPlayer().getConnection();
|
||||||
ConnectedPlayer player = serverConn.getPlayer();
|
|
||||||
ClientConfigSessionHandler configHandler =
|
final ServerResourcePackRemoveEvent event = new ServerResourcePackRemoveEvent(
|
||||||
(ClientConfigSessionHandler) player.getConnection().getActiveSessionHandler();
|
packet.getId(), this.serverConn);
|
||||||
|
server.getEventManager().fire(event).thenAcceptAsync(serverResourcePackRemoveEvent -> {
|
||||||
|
if (playerConnection.isClosed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (serverResourcePackRemoveEvent.getResult().isAllowed()) {
|
||||||
|
final ConnectedPlayer player = serverConn.getPlayer();
|
||||||
|
final ResourcePackHandler handler = player.resourcePackHandler();
|
||||||
|
if (packet.getId() != null) {
|
||||||
|
handler.remove(packet.getId());
|
||||||
|
} else {
|
||||||
|
handler.clearAppliedResourcePacks();
|
||||||
|
}
|
||||||
|
playerConnection.write(packet);
|
||||||
|
}
|
||||||
|
}, playerConnection.eventLoop()).exceptionally((ex) -> {
|
||||||
|
logger.error("Exception while handling resource pack remove for {}", playerConnection, ex);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handle(FinishedUpdatePacket packet) {
|
||||||
|
final MinecraftConnection smc = serverConn.ensureConnected();
|
||||||
|
final ConnectedPlayer player = serverConn.getPlayer();
|
||||||
|
final ClientConfigSessionHandler configHandler = (ClientConfigSessionHandler) player.getConnection().getActiveSessionHandler();
|
||||||
|
|
||||||
smc.setAutoReading(false);
|
|
||||||
// Even when not auto reading messages are still decoded. Decode them with the correct state
|
|
||||||
smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.PLAY);
|
smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.PLAY);
|
||||||
configHandler.handleBackendFinishUpdate(serverConn).thenAcceptAsync((unused) -> {
|
//noinspection DataFlowIssue
|
||||||
|
configHandler.handleBackendFinishUpdate(serverConn).thenRunAsync(() -> {
|
||||||
|
smc.write(FinishedUpdatePacket.INSTANCE);
|
||||||
if (serverConn == player.getConnectedServer()) {
|
if (serverConn == player.getConnectedServer()) {
|
||||||
smc.setActiveSessionHandler(StateRegistry.PLAY);
|
smc.setActiveSessionHandler(StateRegistry.PLAY);
|
||||||
player.sendPlayerListHeaderAndFooter(
|
player.sendPlayerListHeaderAndFooter(player.getPlayerListHeader(), player.getPlayerListFooter());
|
||||||
player.getPlayerListHeader(), player.getPlayerListFooter());
|
|
||||||
// The client cleared the tab list. TODO: Restore changes done via TabList API
|
// The client cleared the tab list. TODO: Restore changes done via TabList API
|
||||||
player.getTabList().clearAllSilent();
|
player.getTabList().clearAllSilent();
|
||||||
} else {
|
} else {
|
||||||
smc.setActiveSessionHandler(StateRegistry.PLAY,
|
smc.setActiveSessionHandler(StateRegistry.PLAY, new TransitionSessionHandler(server, serverConn, resultFuture));
|
||||||
new TransitionSessionHandler(server, serverConn, resultFuture));
|
|
||||||
}
|
}
|
||||||
if (player.resourcePackHandler().getFirstAppliedPack() == null
|
if (player.resourcePackHandler().getFirstAppliedPack() == null && resourcePackToApply != null) {
|
||||||
&& resourcePackToApply != null) {
|
|
||||||
player.resourcePackHandler().queueResourcePack(resourcePackToApply);
|
player.resourcePackHandler().queueResourcePack(resourcePackToApply);
|
||||||
}
|
}
|
||||||
smc.setAutoReading(true);
|
|
||||||
}, smc.eventLoop());
|
}, smc.eventLoop());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ package com.velocitypowered.proxy.connection.backend;
|
|||||||
|
|
||||||
import com.velocitypowered.api.event.player.CookieRequestEvent;
|
import com.velocitypowered.api.event.player.CookieRequestEvent;
|
||||||
import com.velocitypowered.api.event.player.ServerLoginPluginMessageEvent;
|
import com.velocitypowered.api.event.player.ServerLoginPluginMessageEvent;
|
||||||
|
import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
@ -142,10 +143,8 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(ServerLoginSuccessPacket packet) {
|
public boolean handle(ServerLoginSuccessPacket packet) {
|
||||||
if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN
|
if (server.getConfiguration().getPlayerInfoForwardingMode() == PlayerInfoForwarding.MODERN && !informationForwarded) {
|
||||||
&& !informationForwarded) {
|
resultFuture.complete(ConnectionRequestResults.forDisconnect(MODERN_IP_FORWARDING_FAILURE, serverConn.getServer()));
|
||||||
resultFuture.complete(ConnectionRequestResults.forDisconnect(MODERN_IP_FORWARDING_FAILURE,
|
|
||||||
serverConn.getServer()));
|
|
||||||
serverConn.disconnect();
|
serverConn.disconnect();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -156,22 +155,20 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
// Move into the PLAY phase.
|
// Move into the PLAY phase.
|
||||||
MinecraftConnection smc = serverConn.ensureConnected();
|
MinecraftConnection smc = serverConn.ensureConnected();
|
||||||
if (smc.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
|
if (smc.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
|
||||||
smc.setActiveSessionHandler(StateRegistry.PLAY,
|
smc.setActiveSessionHandler(StateRegistry.PLAY, new TransitionSessionHandler(server, serverConn, resultFuture));
|
||||||
new TransitionSessionHandler(server, serverConn, resultFuture));
|
|
||||||
} else {
|
} else {
|
||||||
smc.write(new LoginAcknowledgedPacket());
|
smc.write(new LoginAcknowledgedPacket());
|
||||||
smc.setActiveSessionHandler(StateRegistry.CONFIG,
|
smc.setActiveSessionHandler(StateRegistry.CONFIG, new ConfigSessionHandler(server, serverConn, resultFuture));
|
||||||
new ConfigSessionHandler(server, serverConn, resultFuture));
|
|
||||||
ConnectedPlayer player = serverConn.getPlayer();
|
ConnectedPlayer player = serverConn.getPlayer();
|
||||||
if (player.getClientSettingsPacket() != null) {
|
if (player.getClientSettingsPacket() != null) {
|
||||||
smc.write(player.getClientSettingsPacket());
|
smc.write(player.getClientSettingsPacket());
|
||||||
}
|
}
|
||||||
if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler) {
|
if (player.getConnection().getActiveSessionHandler() instanceof ClientPlaySessionHandler clientPlaySessionHandler) {
|
||||||
smc.setAutoReading(false);
|
smc.setAutoReading(false);
|
||||||
((ClientPlaySessionHandler) player.getConnection()
|
clientPlaySessionHandler.doSwitch().thenAcceptAsync((unused) -> smc.setAutoReading(true), smc.eventLoop());
|
||||||
.getActiveSessionHandler()).doSwitch().thenAcceptAsync((unused) -> {
|
} else {
|
||||||
smc.setAutoReading(true);
|
// Initial login - the player is already in configuration state.
|
||||||
}, smc.eventLoop());
|
server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConn));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,7 +208,7 @@ public class LoginSessionHandler implements MinecraftSessionHandler {
|
|||||||
The connection to the remote server was unexpectedly closed.
|
The connection to the remote server was unexpectedly closed.
|
||||||
This is usually because the remote server does not have \
|
This is usually because the remote server does not have \
|
||||||
BungeeCord IP forwarding correctly enabled.
|
BungeeCord IP forwarding correctly enabled.
|
||||||
See https://velocitypowered.com/wiki/users/forwarding/ for instructions \
|
See https://docs.papermc.io/velocity/player-information-forwarding for instructions \
|
||||||
on how to configure player info forwarding correctly."""));
|
on how to configure player info forwarding correctly."""));
|
||||||
} else {
|
} else {
|
||||||
resultFuture.completeExceptionally(
|
resultFuture.completeExceptionally(
|
||||||
|
@ -178,14 +178,14 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
|
|||||||
inbound.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_data"));
|
inbound.disconnect(Component.translatable("multiplayer.disconnect.invalid_player_data"));
|
||||||
} else {
|
} else {
|
||||||
loginState = State.ACKNOWLEDGED;
|
loginState = State.ACKNOWLEDGED;
|
||||||
mcConnection.setActiveSessionHandler(StateRegistry.CONFIG,
|
mcConnection.setActiveSessionHandler(StateRegistry.CONFIG, new ClientConfigSessionHandler(server, connectedPlayer));
|
||||||
new ClientConfigSessionHandler(server, connectedPlayer));
|
|
||||||
|
|
||||||
server.getEventManager().fire(new PostLoginEvent(connectedPlayer))
|
server.getEventManager().fire(new PostLoginEvent(connectedPlayer)).thenCompose(ignored -> {
|
||||||
.thenCompose((ignored) -> connectToInitialServer(connectedPlayer)).exceptionally((ex) -> {
|
return connectToInitialServer(connectedPlayer);
|
||||||
logger.error("Exception while connecting {} to initial server", connectedPlayer, ex);
|
}).exceptionally((ex) -> {
|
||||||
return null;
|
logger.error("Exception while connecting {} to initial server", connectedPlayer, ex);
|
||||||
});
|
return null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -224,8 +224,7 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
|
|||||||
player.disconnect0(reason.get(), true);
|
player.disconnect0(reason.get(), true);
|
||||||
} else {
|
} else {
|
||||||
if (!server.registerConnection(player)) {
|
if (!server.registerConnection(player)) {
|
||||||
player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"),
|
player.disconnect0(Component.translatable("velocity.error.already-connected-proxy"), true);
|
||||||
true);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,13 +237,13 @@ public class AuthSessionHandler implements MinecraftSessionHandler {
|
|||||||
loginState = State.SUCCESS_SENT;
|
loginState = State.SUCCESS_SENT;
|
||||||
if (inbound.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
|
if (inbound.getProtocolVersion().lessThan(ProtocolVersion.MINECRAFT_1_20_2)) {
|
||||||
loginState = State.ACKNOWLEDGED;
|
loginState = State.ACKNOWLEDGED;
|
||||||
mcConnection.setActiveSessionHandler(StateRegistry.PLAY,
|
mcConnection.setActiveSessionHandler(StateRegistry.PLAY, new InitialConnectSessionHandler(player, server));
|
||||||
new InitialConnectSessionHandler(player, server));
|
server.getEventManager().fire(new PostLoginEvent(player)).thenCompose((ignored) -> {
|
||||||
server.getEventManager().fire(new PostLoginEvent(player))
|
return connectToInitialServer(player);
|
||||||
.thenCompose((ignored) -> connectToInitialServer(player)).exceptionally((ex) -> {
|
}).exceptionally((ex) -> {
|
||||||
logger.error("Exception while connecting {} to initial server", player, ex);
|
logger.error("Exception while connecting {} to initial server", player, ex);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, mcConnection.eventLoop()).exceptionally((ex) -> {
|
}, mcConnection.eventLoop()).exceptionally((ex) -> {
|
||||||
|
@ -19,6 +19,9 @@ package com.velocitypowered.proxy.connection.client;
|
|||||||
|
|
||||||
import com.velocitypowered.api.event.player.CookieReceiveEvent;
|
import com.velocitypowered.api.event.player.CookieReceiveEvent;
|
||||||
import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
|
import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
|
||||||
|
import com.velocitypowered.api.event.player.configuration.PlayerConfigurationEvent;
|
||||||
|
import com.velocitypowered.api.event.player.configuration.PlayerFinishConfigurationEvent;
|
||||||
|
import com.velocitypowered.api.event.player.configuration.PlayerFinishedConfigurationEvent;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||||
@ -46,8 +49,6 @@ 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;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the client config stage.
|
* Handles the client config stage.
|
||||||
@ -59,6 +60,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
private final ConnectedPlayer player;
|
private final ConnectedPlayer player;
|
||||||
private String brandChannel = null;
|
private String brandChannel = null;
|
||||||
|
|
||||||
|
private CompletableFuture<?> configurationFuture;
|
||||||
private CompletableFuture<Void> configSwitchFuture;
|
private CompletableFuture<Void> configSwitchFuture;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,13 +79,14 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
configSwitchFuture = new CompletableFuture<>();
|
configSwitchFuture = new CompletableFuture<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deactivated() {
|
||||||
|
configurationFuture = null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(final KeepAlivePacket packet) {
|
public boolean handle(final KeepAlivePacket packet) {
|
||||||
final VelocityServerConnection serverConnection = player.getConnectedServer();
|
player.forwardKeepAlive(packet);
|
||||||
if (!this.sendKeepAliveToBackend(serverConnection, packet)) {
|
|
||||||
final VelocityServerConnection connectionInFlight = player.getConnectionInFlight();
|
|
||||||
this.sendKeepAliveToBackend(connectionInFlight, packet);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,8 +107,7 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(FinishedUpdatePacket packet) {
|
public boolean handle(FinishedUpdatePacket packet) {
|
||||||
player.getConnection()
|
player.getConnection().setActiveSessionHandler(StateRegistry.PLAY, new ClientPlaySessionHandler(server, player));
|
||||||
.setActiveSessionHandler(StateRegistry.PLAY, new ClientPlaySessionHandler(server, player));
|
|
||||||
|
|
||||||
configSwitchFuture.complete(null);
|
configSwitchFuture.complete(null);
|
||||||
return true;
|
return true;
|
||||||
@ -139,12 +141,14 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(KnownPacksPacket packet) {
|
public boolean handle(KnownPacksPacket packet) {
|
||||||
if (player.getConnectionInFlight() != null) {
|
callConfigurationEvent().thenRun(() -> {
|
||||||
player.getConnectionInFlight().ensureConnected().write(packet);
|
player.getConnectionInFlightOrConnectedServer().ensureConnected().write(packet);
|
||||||
return true;
|
}).exceptionally(ex -> {
|
||||||
}
|
logger.error("Error forwarding known packs response to backend:", ex);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -207,26 +211,25 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void exception(Throwable throwable) {
|
public void exception(Throwable throwable) {
|
||||||
player.disconnect(
|
player.disconnect(Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED));
|
||||||
Component.translatable("velocity.error.player-connection-error", NamedTextColor.RED));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean sendKeepAliveToBackend(
|
/**
|
||||||
final @Nullable VelocityServerConnection serverConnection,
|
* Calls the {@link PlayerConfigurationEvent}.
|
||||||
final @NotNull KeepAlivePacket packet
|
* For 1.20.5+ backends this is done when the client responds to
|
||||||
) {
|
* the known packs request. The response is delayed until the event
|
||||||
if (serverConnection != null) {
|
* has been called.
|
||||||
final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId());
|
* For 1.20.2-1.20.4 servers this is done when the client acknowledges
|
||||||
if (sentTime != null) {
|
* the end of the configuration.
|
||||||
final MinecraftConnection smc = serverConnection.getConnection();
|
* This is handled differently because for 1.20.5+ servers can't keep
|
||||||
if (smc != null) {
|
* their connection alive between states and older servers don't have
|
||||||
player.setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime));
|
* the known packs transaction.
|
||||||
smc.write(packet);
|
*/
|
||||||
return true;
|
private CompletableFuture<?> callConfigurationEvent() {
|
||||||
}
|
if (configurationFuture != null) {
|
||||||
}
|
return configurationFuture;
|
||||||
}
|
}
|
||||||
return false;
|
return configurationFuture = server.getEventManager().fire(new PlayerConfigurationEvent(player, player.getConnectionInFlightOrConnectedServer()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -246,14 +249,18 @@ public class ClientConfigSessionHandler implements MinecraftSessionHandler {
|
|||||||
smc.write(brandPacket);
|
smc.write(brandPacket);
|
||||||
}
|
}
|
||||||
|
|
||||||
player.getConnection().eventLoop().execute(() -> {
|
callConfigurationEvent().thenCompose(v -> {
|
||||||
|
return server.getEventManager().fire(new PlayerFinishConfigurationEvent(player, serverConn))
|
||||||
|
.completeOnTimeout(null, 5, TimeUnit.SECONDS);
|
||||||
|
}).thenRunAsync(() -> {
|
||||||
player.getConnection().write(FinishedUpdatePacket.INSTANCE);
|
player.getConnection().write(FinishedUpdatePacket.INSTANCE);
|
||||||
player.getConnection().getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.PLAY);
|
player.getConnection().getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.PLAY);
|
||||||
|
server.getEventManager().fireAndForget(new PlayerFinishedConfigurationEvent(player, serverConn));
|
||||||
|
}, player.getConnection().eventLoop()).exceptionally(ex -> {
|
||||||
|
logger.error("Error finishing configuration state:", ex);
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
|
|
||||||
smc.write(FinishedUpdatePacket.INSTANCE);
|
|
||||||
smc.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.PLAY);
|
|
||||||
|
|
||||||
return configSwitchFuture;
|
return configSwitchFuture;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ import com.velocitypowered.api.event.player.CookieReceiveEvent;
|
|||||||
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent;
|
import com.velocitypowered.api.event.player.PlayerChannelRegisterEvent;
|
||||||
import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
|
import com.velocitypowered.api.event.player.PlayerClientBrandEvent;
|
||||||
import com.velocitypowered.api.event.player.TabCompleteEvent;
|
import com.velocitypowered.api.event.player.TabCompleteEvent;
|
||||||
|
import com.velocitypowered.api.event.player.configuration.PlayerEnteredConfigurationEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
import com.velocitypowered.api.proxy.messages.ChannelIdentifier;
|
||||||
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
|
import com.velocitypowered.api.proxy.messages.LegacyChannelIdentifier;
|
||||||
@ -53,6 +54,7 @@ import com.velocitypowered.proxy.protocol.packet.ServerboundCookieResponsePacket
|
|||||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket;
|
import com.velocitypowered.proxy.protocol.packet.TabCompleteRequestPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
|
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer;
|
import com.velocitypowered.proxy.protocol.packet.TabCompleteResponsePacket.Offer;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.ChatHandler;
|
import com.velocitypowered.proxy.protocol.packet.chat.ChatHandler;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.ChatTimeKeeper;
|
import com.velocitypowered.proxy.protocol.packet.chat.ChatTimeKeeper;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
|
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
|
||||||
@ -84,7 +86,6 @@ import java.util.Queue;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import net.kyori.adventure.key.Key;
|
import net.kyori.adventure.key.Key;
|
||||||
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;
|
||||||
@ -176,17 +177,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(KeepAlivePacket packet) {
|
public boolean handle(KeepAlivePacket packet) {
|
||||||
final VelocityServerConnection serverConnection = player.getConnectedServer();
|
player.forwardKeepAlive(packet);
|
||||||
if (serverConnection != null) {
|
|
||||||
final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId());
|
|
||||||
if (sentTime != null) {
|
|
||||||
final MinecraftConnection smc = serverConnection.getConnection();
|
|
||||||
if (smc != null) {
|
|
||||||
player.setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime));
|
|
||||||
smc.write(packet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -406,6 +397,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
// Complete client switch
|
// Complete client switch
|
||||||
player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG);
|
player.getConnection().setActiveSessionHandler(StateRegistry.CONFIG);
|
||||||
VelocityServerConnection serverConnection = player.getConnectedServer();
|
VelocityServerConnection serverConnection = player.getConnectedServer();
|
||||||
|
server.getEventManager().fireAndForget(new PlayerEnteredConfigurationEvent(player, serverConnection));
|
||||||
if (serverConnection != null) {
|
if (serverConnection != null) {
|
||||||
MinecraftConnection smc = serverConnection.ensureConnected();
|
MinecraftConnection smc = serverConnection.ensureConnected();
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
@ -421,6 +413,15 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handle(ChatAcknowledgementPacket packet) {
|
||||||
|
if (player.getCurrentServer().isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
player.getChatQueue().handleAcknowledgement(packet.offset());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean handle(ServerboundCookieResponsePacket packet) {
|
public boolean handle(ServerboundCookieResponsePacket packet) {
|
||||||
server.getEventManager()
|
server.getEventManager()
|
||||||
@ -512,7 +513,7 @@ public class ClientPlaySessionHandler implements MinecraftSessionHandler {
|
|||||||
* @return a future that completes when the switch is complete
|
* @return a future that completes when the switch is complete
|
||||||
*/
|
*/
|
||||||
public CompletableFuture<Void> doSwitch() {
|
public CompletableFuture<Void> doSwitch() {
|
||||||
VelocityServerConnection existingConnection = player.getConnectedServer();
|
final VelocityServerConnection existingConnection = player.getConnectedServer();
|
||||||
|
|
||||||
if (existingConnection != null) {
|
if (existingConnection != null) {
|
||||||
// Shut down the existing server connection.
|
// Shut down the existing server connection.
|
||||||
|
@ -37,6 +37,7 @@ import com.velocitypowered.api.event.player.KickedFromServerEvent.ServerKickResu
|
|||||||
import com.velocitypowered.api.event.player.PlayerModInfoEvent;
|
import com.velocitypowered.api.event.player.PlayerModInfoEvent;
|
||||||
import com.velocitypowered.api.event.player.PlayerSettingsChangedEvent;
|
import com.velocitypowered.api.event.player.PlayerSettingsChangedEvent;
|
||||||
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
|
import com.velocitypowered.api.event.player.ServerPreConnectEvent;
|
||||||
|
import com.velocitypowered.api.event.player.configuration.PlayerEnterConfigurationEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolState;
|
import com.velocitypowered.api.network.ProtocolState;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.permission.PermissionFunction;
|
import com.velocitypowered.api.permission.PermissionFunction;
|
||||||
@ -54,18 +55,21 @@ import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
|||||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
import com.velocitypowered.api.util.GameProfile;
|
import com.velocitypowered.api.util.GameProfile;
|
||||||
import com.velocitypowered.api.util.ModInfo;
|
import com.velocitypowered.api.util.ModInfo;
|
||||||
|
import com.velocitypowered.api.util.ServerLink;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation;
|
import com.velocitypowered.proxy.adventure.VelocityBossBarImplementation;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
|
import com.velocitypowered.proxy.connection.MinecraftConnectionAssociation;
|
||||||
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
||||||
import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo;
|
import com.velocitypowered.proxy.connection.player.bundle.BundleDelimiterHandler;
|
||||||
import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackHandler;
|
import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo;
|
||||||
|
import com.velocitypowered.proxy.connection.player.resourcepack.handler.ResourcePackHandler;
|
||||||
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
|
import com.velocitypowered.proxy.connection.util.ConnectionMessages;
|
||||||
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl;
|
import com.velocitypowered.proxy.connection.util.ConnectionRequestResults.Impl;
|
||||||
import com.velocitypowered.proxy.connection.util.VelocityInboundConnection;
|
import com.velocitypowered.proxy.connection.util.VelocityInboundConnection;
|
||||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||||
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
|
import com.velocitypowered.proxy.protocol.netty.MinecraftEncoder;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
|
import com.velocitypowered.proxy.protocol.packet.ClientSettingsPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
|
import com.velocitypowered.proxy.protocol.packet.ClientboundCookieRequestPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
|
import com.velocitypowered.proxy.protocol.packet.ClientboundStoreCookiePacket;
|
||||||
@ -80,7 +84,9 @@ import com.velocitypowered.proxy.protocol.packet.chat.ChatType;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
|
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket;
|
import com.velocitypowered.proxy.protocol.packet.chat.PlayerChatCompletionPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory;
|
import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderFactory;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.chat.builder.ChatBuilderV2;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChatPacket;
|
import com.velocitypowered.proxy.protocol.packet.chat.legacy.LegacyChatPacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.config.ClientboundServerLinksPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket;
|
import com.velocitypowered.proxy.protocol.packet.config.StartUpdatePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket;
|
import com.velocitypowered.proxy.protocol.packet.title.GenericTitlePacket;
|
||||||
import com.velocitypowered.proxy.protocol.util.ByteBufDataOutput;
|
import com.velocitypowered.proxy.protocol.util.ByteBufDataOutput;
|
||||||
@ -106,6 +112,7 @@ import java.util.UUID;
|
|||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
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.identity.Identity;
|
||||||
@ -629,6 +636,10 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
return connectionInFlight;
|
return connectionInFlight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public VelocityServerConnection getConnectionInFlightOrConnectedServer() {
|
||||||
|
return connectionInFlight != null ? connectionInFlight : connectedServer;
|
||||||
|
}
|
||||||
|
|
||||||
public void resetInFlightConnection() {
|
public void resetInFlightConnection() {
|
||||||
connectionInFlight = null;
|
connectionInFlight = null;
|
||||||
}
|
}
|
||||||
@ -806,7 +817,7 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
}, connection.eventLoop());
|
}, connection.eventLoop());
|
||||||
} else if (event.getResult() instanceof final Notify res) {
|
} else if (event.getResult() instanceof final Notify res) {
|
||||||
if (event.kickedDuringServerConnect() && previousConnection != null) {
|
if (event.kickedDuringServerConnect() && previousConnection != null) {
|
||||||
sendMessage(Identity.nil(), res.getMessageComponent());
|
sendMessage(res.getMessageComponent());
|
||||||
} else {
|
} else {
|
||||||
disconnect(res.getMessageComponent());
|
disconnect(res.getMessageComponent());
|
||||||
}
|
}
|
||||||
@ -1057,6 +1068,22 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
}, connection.eventLoop());
|
}, connection.eventLoop());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setServerLinks(final @NotNull List<ServerLink> links) {
|
||||||
|
Preconditions.checkNotNull(links, "links");
|
||||||
|
Preconditions.checkArgument(
|
||||||
|
this.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_21),
|
||||||
|
"Player version must be at least 1.21 to be able to set server links");
|
||||||
|
|
||||||
|
if (connection.getState() != StateRegistry.PLAY
|
||||||
|
&& connection.getState() != StateRegistry.CONFIG) {
|
||||||
|
throw new IllegalStateException("Can only send server links in CONFIGURATION or PLAY protocol");
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.write(new ClientboundServerLinksPacket(List.copyOf(links).stream()
|
||||||
|
.map(l -> new ClientboundServerLinksPacket.ServerLink(l, getProtocolVersion())).toList()));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addCustomChatCompletions(@NotNull Collection<String> completions) {
|
public void addCustomChatCompletions(@NotNull Collection<String> completions) {
|
||||||
Preconditions.checkNotNull(completions, "completions");
|
Preconditions.checkNotNull(completions, "completions");
|
||||||
@ -1088,11 +1115,12 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
"input cannot be greater than " + LegacyChatPacket.MAX_SERVERBOUND_MESSAGE_LENGTH
|
"input cannot be greater than " + LegacyChatPacket.MAX_SERVERBOUND_MESSAGE_LENGTH
|
||||||
+ " characters in length");
|
+ " characters in length");
|
||||||
if (getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19)) {
|
if (getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19)) {
|
||||||
this.chatQueue.hijack(getChatBuilderFactory().builder().asPlayer(this).message(input),
|
ChatBuilderV2 message = getChatBuilderFactory().builder().asPlayer(this).message(input);
|
||||||
(instant, item) -> {
|
this.chatQueue.queuePacket(chatState -> {
|
||||||
item.setTimestamp(instant);
|
message.setTimestamp(chatState.lastTimestamp);
|
||||||
return item.toServer();
|
message.setLastSeenMessages(chatState.createLastSeen());
|
||||||
});
|
return message.toServer();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
ensureBackendConnection().write(getChatBuilderFactory().builder()
|
ensureBackendConnection().write(getChatBuilderFactory().builder()
|
||||||
.asPlayer(this).message(input).toServer());
|
.asPlayer(this).message(input).toServer());
|
||||||
@ -1217,20 +1245,50 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forwards the keep alive packet to the backend server it belongs to.
|
||||||
|
* This is either the connection in flight or the connected server.
|
||||||
|
*/
|
||||||
|
public boolean forwardKeepAlive(final KeepAlivePacket packet) {
|
||||||
|
if (!this.sendKeepAliveToBackend(connectedServer, packet)) {
|
||||||
|
return this.sendKeepAliveToBackend(connectionInFlight, packet);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sendKeepAliveToBackend(final @Nullable VelocityServerConnection serverConnection, final @NotNull KeepAlivePacket packet) {
|
||||||
|
if (serverConnection != null) {
|
||||||
|
final Long sentTime = serverConnection.getPendingPings().remove(packet.getRandomId());
|
||||||
|
if (sentTime != null) {
|
||||||
|
final MinecraftConnection smc = serverConnection.getConnection();
|
||||||
|
if (smc != null) {
|
||||||
|
setPing(TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - sentTime));
|
||||||
|
smc.write(packet);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switches the connection to the client into config state.
|
* Switches the connection to the client into config state.
|
||||||
*/
|
*/
|
||||||
public void switchToConfigState() {
|
public void switchToConfigState() {
|
||||||
CompletableFuture.runAsync(() -> {
|
server.getEventManager().fire(new PlayerEnterConfigurationEvent(this, getConnectionInFlightOrConnectedServer()))
|
||||||
connection.write(StartUpdatePacket.INSTANCE);
|
.completeOnTimeout(null, 5, TimeUnit.SECONDS).thenRunAsync(() -> {
|
||||||
connection.getChannel().pipeline()
|
if (bundleHandler.isInBundleSession()) {
|
||||||
.get(MinecraftEncoder.class).setState(StateRegistry.CONFIG);
|
bundleHandler.toggleBundleSession();
|
||||||
// Make sure we don't send any play packets to the player after update start
|
connection.write(BundleDelimiterPacket.INSTANCE);
|
||||||
connection.addPlayPacketQueueHandler();
|
}
|
||||||
}, connection.eventLoop()).exceptionally((ex) -> {
|
connection.write(StartUpdatePacket.INSTANCE);
|
||||||
logger.error("Error switching player connection to config state:", ex);
|
connection.getChannel().pipeline().get(MinecraftEncoder.class).setState(StateRegistry.CONFIG);
|
||||||
return null;
|
// Make sure we don't send any play packets to the player after update start
|
||||||
});
|
connection.addPlayPacketQueueHandler();
|
||||||
|
}, connection.eventLoop()).exceptionally((ex) -> {
|
||||||
|
logger.error("Error switching player connection to config state", ex);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1363,24 +1421,20 @@ public class ConnectedPlayer implements MinecraftConnectionAssociation, Player,
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (status.getStatus()) {
|
switch (status.getStatus()) {
|
||||||
case ALREADY_CONNECTED:
|
case ALREADY_CONNECTED -> sendMessage(ConnectionMessages.ALREADY_CONNECTED);
|
||||||
sendMessage(Identity.nil(), ConnectionMessages.ALREADY_CONNECTED);
|
case CONNECTION_IN_PROGRESS -> sendMessage(ConnectionMessages.IN_PROGRESS);
|
||||||
break;
|
case CONNECTION_CANCELLED -> {
|
||||||
case CONNECTION_IN_PROGRESS:
|
|
||||||
sendMessage(Identity.nil(), ConnectionMessages.IN_PROGRESS);
|
|
||||||
break;
|
|
||||||
case CONNECTION_CANCELLED:
|
|
||||||
// Ignored; the plugin probably already handled this.
|
// Ignored; the plugin probably already handled this.
|
||||||
break;
|
}
|
||||||
case SERVER_DISCONNECTED:
|
case SERVER_DISCONNECTED -> {
|
||||||
Component reason = status.getReasonComponent()
|
final Component reason = status.getReasonComponent()
|
||||||
.orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR);
|
.orElse(ConnectionMessages.INTERNAL_SERVER_CONNECTION_ERROR);
|
||||||
handleConnectionException(toConnect,
|
handleConnectionException(toConnect,
|
||||||
DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()), status.isSafe());
|
DisconnectPacket.create(reason, getProtocolVersion(), connection.getState()), status.isSafe());
|
||||||
break;
|
}
|
||||||
default:
|
default -> {
|
||||||
// The only remaining value is successful (no need to do anything!)
|
// The only remaining value is successful (no need to do anything!)
|
||||||
break;
|
}
|
||||||
}
|
}
|
||||||
}, connection.eventLoop()).thenApply(Result::isSuccessful);
|
}, connection.eventLoop()).thenApply(Result::isSuccessful);
|
||||||
}
|
}
|
||||||
|
@ -15,10 +15,11 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.velocitypowered.proxy.connection.client;
|
package com.velocitypowered.proxy.connection.player.bundle;
|
||||||
|
|
||||||
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
import com.velocitypowered.proxy.connection.MinecraftConnection;
|
||||||
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
||||||
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
import com.velocitypowered.proxy.protocol.StateRegistry;
|
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||||
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
|
import com.velocitypowered.proxy.protocol.packet.BundleDelimiterPacket;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
@ -15,7 +15,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.velocitypowered.proxy.connection.player;
|
package com.velocitypowered.proxy.connection.player.resourcepack;
|
||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
@ -15,7 +15,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.velocitypowered.proxy.connection.player.resourcepack;
|
package com.velocitypowered.proxy.connection.player.resourcepack.handler;
|
||||||
|
|
||||||
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
@ -15,13 +15,14 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.velocitypowered.proxy.connection.player.resourcepack;
|
package com.velocitypowered.proxy.connection.player.resourcepack.handler;
|
||||||
|
|
||||||
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
|
import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
@ -15,7 +15,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.velocitypowered.proxy.connection.player.resourcepack;
|
package com.velocitypowered.proxy.connection.player.resourcepack.handler;
|
||||||
|
|
||||||
import com.google.common.collect.ListMultimap;
|
import com.google.common.collect.ListMultimap;
|
||||||
import com.google.common.collect.Multimaps;
|
import com.google.common.collect.Multimaps;
|
||||||
@ -23,6 +23,7 @@ import com.velocitypowered.api.event.player.PlayerResourcePackStatusEvent;
|
|||||||
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
|
import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
@ -15,14 +15,15 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.velocitypowered.proxy.connection.player.resourcepack;
|
package com.velocitypowered.proxy.connection.player.resourcepack.handler;
|
||||||
|
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
import com.velocitypowered.proxy.connection.backend.VelocityServerConnection;
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo;
|
import com.velocitypowered.proxy.connection.player.resourcepack.ResourcePackResponseBundle;
|
||||||
|
import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ResourcePackRequestPacket;
|
import com.velocitypowered.proxy.protocol.packet.ResourcePackRequestPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
|
import com.velocitypowered.proxy.protocol.packet.ResourcePackResponsePacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
|
import com.velocitypowered.proxy.protocol.packet.chat.ComponentHolder;
|
@ -35,7 +35,8 @@ public class Connections {
|
|||||||
public static final String MINECRAFT_DECODER = "minecraft-decoder";
|
public static final String MINECRAFT_DECODER = "minecraft-decoder";
|
||||||
public static final String MINECRAFT_ENCODER = "minecraft-encoder";
|
public static final String MINECRAFT_ENCODER = "minecraft-encoder";
|
||||||
public static final String READ_TIMEOUT = "read-timeout";
|
public static final String READ_TIMEOUT = "read-timeout";
|
||||||
public static final String PLAY_PACKET_QUEUE = "play-packet-queue";
|
public static final String PLAY_PACKET_QUEUE_OUTBOUND = "play-packet-queue-outbound";
|
||||||
|
public static final String PLAY_PACKET_QUEUE_INBOUND = "play-packet-queue-inbound";
|
||||||
|
|
||||||
private Connections() {
|
private Connections() {
|
||||||
throw new AssertionError();
|
throw new AssertionError();
|
||||||
|
@ -43,13 +43,13 @@ import java.nio.file.Path;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashMap;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
@ -86,35 +86,48 @@ public class VelocityPluginManager implements PluginManager {
|
|||||||
checkNotNull(directory, "directory");
|
checkNotNull(directory, "directory");
|
||||||
checkArgument(directory.toFile().isDirectory(), "provided path isn't a directory");
|
checkArgument(directory.toFile().isDirectory(), "provided path isn't a directory");
|
||||||
|
|
||||||
List<PluginDescription> found = new ArrayList<>();
|
Map<String, PluginDescription> foundCandidates = new LinkedHashMap<>();
|
||||||
JavaPluginLoader loader = new JavaPluginLoader(server, directory);
|
JavaPluginLoader loader = new JavaPluginLoader(server, directory);
|
||||||
|
|
||||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory,
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory,
|
||||||
p -> p.toFile().isFile() && p.toString().endsWith(".jar"))) {
|
p -> p.toFile().isFile() && p.toString().endsWith(".jar"))) {
|
||||||
for (Path path : stream) {
|
for (Path path : stream) {
|
||||||
try {
|
try {
|
||||||
found.add(loader.loadCandidate(path));
|
PluginDescription candidate = loader.loadCandidate(path);
|
||||||
|
|
||||||
|
// If we found a duplicate candidate (with the same ID), don't load it.
|
||||||
|
PluginDescription maybeExistingCandidate = foundCandidates.putIfAbsent(
|
||||||
|
candidate.getId(), candidate);
|
||||||
|
|
||||||
|
if (maybeExistingCandidate != null) {
|
||||||
|
logger.error("Refusing to load plugin at path {} since we already "
|
||||||
|
+ "loaded a plugin with the same ID {} from {}",
|
||||||
|
candidate.getSource().map(Objects::toString).orElse("<UNKNOWN>"),
|
||||||
|
candidate.getId(),
|
||||||
|
maybeExistingCandidate.getSource().map(Objects::toString).orElse("<UNKNOWN>"));
|
||||||
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
logger.error("Unable to load plugin {}", path, e);
|
logger.error("Unable to load plugin {}", path, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (found.isEmpty()) {
|
if (foundCandidates.isEmpty()) {
|
||||||
// No plugins found
|
// No plugins found
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(found);
|
List<PluginDescription> sortedPlugins = PluginDependencyUtils.sortCandidates(
|
||||||
|
new ArrayList<>(foundCandidates.values()));
|
||||||
|
|
||||||
Set<String> loadedPluginsById = new HashSet<>();
|
Map<String, PluginDescription> loadedCandidates = new HashMap<>();
|
||||||
Map<PluginContainer, Module> pluginContainers = new LinkedHashMap<>();
|
Map<PluginContainer, Module> pluginContainers = new LinkedHashMap<>();
|
||||||
// Now load the plugins
|
// Now load the plugins
|
||||||
pluginLoad:
|
pluginLoad:
|
||||||
for (PluginDescription candidate : sortedPlugins) {
|
for (PluginDescription candidate : sortedPlugins) {
|
||||||
// Verify dependencies
|
// Verify dependencies
|
||||||
for (PluginDependency dependency : candidate.getDependencies()) {
|
for (PluginDependency dependency : candidate.getDependencies()) {
|
||||||
if (!dependency.isOptional() && !loadedPluginsById.contains(dependency.getId())) {
|
if (!dependency.isOptional() && !loadedCandidates.containsKey(dependency.getId())) {
|
||||||
logger.error("Can't load plugin {} due to missing dependency {}", candidate.getId(),
|
logger.error("Can't load plugin {} due to missing dependency {}", candidate.getId(),
|
||||||
dependency.getId());
|
dependency.getId());
|
||||||
continue pluginLoad;
|
continue pluginLoad;
|
||||||
@ -125,7 +138,7 @@ public class VelocityPluginManager implements PluginManager {
|
|||||||
PluginDescription realPlugin = loader.createPluginFromCandidate(candidate);
|
PluginDescription realPlugin = loader.createPluginFromCandidate(candidate);
|
||||||
VelocityPluginContainer container = new VelocityPluginContainer(realPlugin);
|
VelocityPluginContainer container = new VelocityPluginContainer(realPlugin);
|
||||||
pluginContainers.put(container, loader.createModule(container));
|
pluginContainers.put(container, loader.createModule(container));
|
||||||
loadedPluginsById.add(realPlugin.getId());
|
loadedCandidates.put(realPlugin.getId(), realPlugin);
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
logger.error("Can't create module for plugin {}", candidate.getId(), e);
|
logger.error("Can't create module for plugin {}", candidate.getId(), e);
|
||||||
}
|
}
|
||||||
|
@ -56,8 +56,7 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||||
if (msg instanceof ByteBuf) {
|
if (msg instanceof ByteBuf buf) {
|
||||||
ByteBuf buf = (ByteBuf) msg;
|
|
||||||
tryDecode(ctx, buf);
|
tryDecode(ctx, buf);
|
||||||
} else {
|
} else {
|
||||||
ctx.fireChannelRead(msg);
|
ctx.fireChannelRead(msg);
|
||||||
@ -147,4 +146,8 @@ public class MinecraftDecoder extends ChannelInboundHandlerAdapter {
|
|||||||
this.state = state;
|
this.state = state;
|
||||||
this.setProtocolVersion(registry.version);
|
this.setProtocolVersion(registry.version);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ProtocolUtils.Direction getDirection() {
|
||||||
|
return direction;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Velocity 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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.velocitypowered.proxy.protocol.netty;
|
||||||
|
|
||||||
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
|
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||||
|
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||||
|
import com.velocitypowered.proxy.protocol.StateRegistry;
|
||||||
|
import io.netty.channel.ChannelDuplexHandler;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.util.ReferenceCountUtil;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Queue;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queues up any pending PLAY packets while the client is in the CONFIG state.
|
||||||
|
*
|
||||||
|
* <p>Much of the Velocity API (i.e. chat messages) utilize PLAY packets, however the client is
|
||||||
|
* incapable of receiving these packets during the CONFIG state. Certain events such as the
|
||||||
|
* ServerPreConnectEvent may be called during this time, and we need to ensure that any API that
|
||||||
|
* uses these packets will work as expected.
|
||||||
|
*
|
||||||
|
* <p>This handler will queue up any packets that are sent to the client during this time, and send
|
||||||
|
* them once the client has (re)entered the PLAY state.
|
||||||
|
*/
|
||||||
|
public class PlayPacketQueueInboundHandler extends ChannelDuplexHandler {
|
||||||
|
|
||||||
|
private final StateRegistry.PacketRegistry.ProtocolRegistry registry;
|
||||||
|
private final Queue<Object> queue = new ArrayDeque<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides registries for client & server bound packets.
|
||||||
|
*
|
||||||
|
* @param version the protocol version
|
||||||
|
*/
|
||||||
|
public PlayPacketQueueInboundHandler(ProtocolVersion version, ProtocolUtils.Direction direction) {
|
||||||
|
this.registry = StateRegistry.CONFIG.getProtocolRegistry(direction, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
|
||||||
|
if (msg instanceof final MinecraftPacket packet) {
|
||||||
|
// If the packet exists in the CONFIG state, we want to always
|
||||||
|
// ensure that it gets handled by the current handler
|
||||||
|
if (this.registry.containsPacket(packet)) {
|
||||||
|
ctx.fireChannelRead(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, queue the packet
|
||||||
|
this.queue.offer(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void channelInactive(@NotNull ChannelHandlerContext ctx) throws Exception {
|
||||||
|
this.releaseQueue(ctx, false);
|
||||||
|
|
||||||
|
super.channelInactive(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
|
||||||
|
this.releaseQueue(ctx, ctx.channel().isActive());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseQueue(ChannelHandlerContext ctx, boolean active) {
|
||||||
|
// Handle all the queued packets
|
||||||
|
Object msg;
|
||||||
|
while ((msg = this.queue.poll()) != null) {
|
||||||
|
if (active) {
|
||||||
|
ctx.fireChannelRead(msg);
|
||||||
|
} else {
|
||||||
|
ReferenceCountUtil.release(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -25,7 +25,7 @@ import io.netty.channel.ChannelDuplexHandler;
|
|||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.channel.ChannelPromise;
|
import io.netty.channel.ChannelPromise;
|
||||||
import io.netty.util.ReferenceCountUtil;
|
import io.netty.util.ReferenceCountUtil;
|
||||||
import io.netty.util.internal.PlatformDependent;
|
import java.util.ArrayDeque;
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
@ -40,38 +40,36 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
* <p>This handler will queue up any packets that are sent to the client during this time, and send
|
* <p>This handler will queue up any packets that are sent to the client during this time, and send
|
||||||
* them once the client has (re)entered the PLAY state.
|
* them once the client has (re)entered the PLAY state.
|
||||||
*/
|
*/
|
||||||
public class PlayPacketQueueHandler extends ChannelDuplexHandler {
|
public class PlayPacketQueueOutboundHandler extends ChannelDuplexHandler {
|
||||||
|
|
||||||
private final StateRegistry.PacketRegistry.ProtocolRegistry registry;
|
private final StateRegistry.PacketRegistry.ProtocolRegistry registry;
|
||||||
private final Queue<MinecraftPacket> queue = PlatformDependent.newMpscQueue();
|
private final Queue<MinecraftPacket> queue = new ArrayDeque<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides registries for client & server bound packets.
|
* Provides registries for client & server bound packets.
|
||||||
*
|
*
|
||||||
* @param version the protocol version
|
* @param version the protocol version
|
||||||
*/
|
*/
|
||||||
public PlayPacketQueueHandler(ProtocolVersion version, ProtocolUtils.Direction direction) {
|
public PlayPacketQueueOutboundHandler(ProtocolVersion version, ProtocolUtils.Direction direction) {
|
||||||
this.registry =
|
this.registry = StateRegistry.CONFIG.getProtocolRegistry(direction, version);
|
||||||
StateRegistry.CONFIG.getProtocolRegistry(direction, version);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
|
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
|
||||||
throws Exception {
|
if (!(msg instanceof final MinecraftPacket packet)) {
|
||||||
if (!(msg instanceof MinecraftPacket)) {
|
|
||||||
ctx.write(msg, promise);
|
ctx.write(msg, promise);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the packet exists in the CONFIG state, we want to always
|
// If the packet exists in the CONFIG state, we want to always
|
||||||
// ensure that it gets sent out to the client
|
// ensure that it gets sent out to the client
|
||||||
if (this.registry.containsPacket(((MinecraftPacket) msg))) {
|
if (this.registry.containsPacket(packet)) {
|
||||||
ctx.write(msg, promise);
|
ctx.write(msg, promise);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, queue the packet
|
// Otherwise, queue the packet
|
||||||
this.queue.offer((MinecraftPacket) msg);
|
this.queue.offer(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -87,10 +85,6 @@ public class PlayPacketQueueHandler extends ChannelDuplexHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void releaseQueue(ChannelHandlerContext ctx, boolean active) {
|
private void releaseQueue(ChannelHandlerContext ctx, boolean active) {
|
||||||
if (this.queue.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send out all the queued packets
|
// Send out all the queued packets
|
||||||
MinecraftPacket packet;
|
MinecraftPacket packet;
|
||||||
while ((packet = this.queue.poll()) != null) {
|
while ((packet = this.queue.poll()) != null) {
|
@ -21,7 +21,7 @@ import com.google.common.base.Preconditions;
|
|||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
import com.velocitypowered.api.proxy.player.ResourcePackInfo;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
||||||
import com.velocitypowered.proxy.connection.player.VelocityResourcePackInfo;
|
import com.velocitypowered.proxy.connection.player.resourcepack.VelocityResourcePackInfo;
|
||||||
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 com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
|
import com.velocitypowered.proxy.protocol.ProtocolUtils.Direction;
|
||||||
|
@ -54,4 +54,8 @@ public class ChatAcknowledgementPacket implements MinecraftPacket {
|
|||||||
"offset=" + offset +
|
"offset=" + offset +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int offset() {
|
||||||
|
return offset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,9 @@ import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
|||||||
import io.netty.channel.ChannelFuture;
|
import io.netty.channel.ChannelFuture;
|
||||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.BitSet;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,9 +34,10 @@ import java.util.function.Function;
|
|||||||
*/
|
*/
|
||||||
public class ChatQueue {
|
public class ChatQueue {
|
||||||
|
|
||||||
private final Object internalLock;
|
private final Object internalLock = new Object();
|
||||||
private final ConnectedPlayer player;
|
private final ConnectedPlayer player;
|
||||||
private CompletableFuture<WrappedPacket> packetFuture;
|
private final ChatState chatState = new ChatState();
|
||||||
|
private CompletableFuture<Void> head = CompletableFuture.completedFuture(null);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Instantiates a {@link ChatQueue} for a specific {@link ConnectedPlayer}.
|
* Instantiates a {@link ChatQueue} for a specific {@link ConnectedPlayer}.
|
||||||
@ -43,8 +46,19 @@ public class ChatQueue {
|
|||||||
*/
|
*/
|
||||||
public ChatQueue(ConnectedPlayer player) {
|
public ChatQueue(ConnectedPlayer player) {
|
||||||
this.player = player;
|
this.player = player;
|
||||||
this.packetFuture = CompletableFuture.completedFuture(new WrappedPacket(Instant.EPOCH, null));
|
}
|
||||||
this.internalLock = new Object();
|
|
||||||
|
private void queueTask(Task task) {
|
||||||
|
synchronized (internalLock) {
|
||||||
|
MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected();
|
||||||
|
head = head.thenCompose(v -> {
|
||||||
|
try {
|
||||||
|
return task.update(chatState, smc).exceptionally(ignored -> null);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -52,121 +66,115 @@ public class ChatQueue {
|
|||||||
* packets. This maintains order on the server-level for the client insertions of commands
|
* packets. This maintains order on the server-level for the client insertions of commands
|
||||||
* and messages. All entries are locked through an internal object lock.
|
* and messages. All entries are locked through an internal object lock.
|
||||||
*
|
*
|
||||||
* @param nextPacket the {@link CompletableFuture} which will provide the next-processed packet.
|
* @param nextPacket a function mapping {@link LastSeenMessages} state to a {@link CompletableFuture} that will
|
||||||
* @param timestamp the {@link Instant} timestamp of this packet so we can allow piggybacking.
|
* provide the next-processed packet. This should include the fixed {@link LastSeenMessages}.
|
||||||
|
* @param timestamp the new {@link Instant} timestamp of this packet to update the internal chat state.
|
||||||
|
* @param lastSeenMessages the new {@link LastSeenMessages} last seen messages to update the internal chat state.
|
||||||
*/
|
*/
|
||||||
public void queuePacket(CompletableFuture<MinecraftPacket> nextPacket, Instant timestamp) {
|
public void queuePacket(Function<LastSeenMessages, CompletableFuture<MinecraftPacket>> nextPacket, @Nullable Instant timestamp, @Nullable LastSeenMessages lastSeenMessages) {
|
||||||
synchronized (internalLock) { // wait for the lock to resolve - we don't want to drop packets
|
queueTask((chatState, smc) -> {
|
||||||
MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected();
|
LastSeenMessages newLastSeenMessages = chatState.updateFromMessage(timestamp, lastSeenMessages);
|
||||||
|
return nextPacket.apply(newLastSeenMessages).thenCompose(packet -> writePacket(packet, smc));
|
||||||
CompletableFuture<WrappedPacket> nextInLine = WrappedPacket.wrap(timestamp, nextPacket);
|
});
|
||||||
this.packetFuture = awaitChat(smc, this.packetFuture,
|
|
||||||
nextInLine); // we await chat, binding `this.packetFuture` -> `nextInLine`
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hijacks the latest sent packet's timestamp to provide an in-order packet without polling the
|
* Hijacks the latest sent packet's chat state to provide an in-order packet without polling the
|
||||||
* physical, or prior packets sent through the stream.
|
* physical, or prior packets sent through the stream.
|
||||||
*
|
*
|
||||||
* @param packet the {@link MinecraftPacket} to send.
|
* @param packetFunction a function that maps the prior {@link ChatState} into a new packet.
|
||||||
* @param instantMapper the {@link InstantPacketMapper} which maps the prior timestamp and current
|
* @param <T> the type of packet to send.
|
||||||
* packet to a new packet.
|
|
||||||
* @param <K> the type of base to expect when mapping the packet.
|
|
||||||
* @param <V> the type of packet for instantMapper type-checking.
|
|
||||||
*/
|
*/
|
||||||
public <K, V extends MinecraftPacket> void hijack(K packet,
|
public <T extends MinecraftPacket> void queuePacket(Function<ChatState, T> packetFunction) {
|
||||||
InstantPacketMapper<K, V> instantMapper) {
|
queueTask((chatState, smc) -> {
|
||||||
synchronized (internalLock) {
|
T packet = packetFunction.apply(chatState);
|
||||||
CompletableFuture<K> trueFuture = CompletableFuture.completedFuture(packet);
|
return writePacket(packet, smc);
|
||||||
MinecraftConnection smc = player.ensureAndGetCurrentServer().ensureConnected();
|
});
|
||||||
|
|
||||||
this.packetFuture = hijackCurrentPacket(smc, this.packetFuture, trueFuture, instantMapper);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Function<WrappedPacket, WrappedPacket> writePacket(MinecraftConnection connection) {
|
public void handleAcknowledgement(int offset) {
|
||||||
return wrappedPacket -> {
|
queueTask((chatState, smc) -> {
|
||||||
if (!connection.isClosed()) {
|
int ackCountToForward = chatState.accumulateAckCount(offset);
|
||||||
ChannelFuture future = wrappedPacket.write(connection);
|
if (ackCountToForward > 0) {
|
||||||
|
return writePacket(new ChatAcknowledgementPacket(ackCountToForward), smc);
|
||||||
|
}
|
||||||
|
return CompletableFuture.completedFuture(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T extends MinecraftPacket> CompletableFuture<Void> writePacket(T packet, MinecraftConnection smc) {
|
||||||
|
return CompletableFuture.runAsync(() -> {
|
||||||
|
if (!smc.isClosed()) {
|
||||||
|
ChannelFuture future = smc.write(packet);
|
||||||
if (future != null) {
|
if (future != null) {
|
||||||
future.awaitUninterruptibly();
|
future.awaitUninterruptibly();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, smc.eventLoop());
|
||||||
return wrappedPacket;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static <T extends MinecraftPacket> CompletableFuture<WrappedPacket> awaitChat(
|
private interface Task {
|
||||||
MinecraftConnection connection,
|
CompletableFuture<Void> update(ChatState chatState, MinecraftConnection smc);
|
||||||
CompletableFuture<WrappedPacket> binder,
|
|
||||||
CompletableFuture<WrappedPacket> future
|
|
||||||
) {
|
|
||||||
// the binder will run -> then the future will get the `write packet` caller
|
|
||||||
return binder.thenCompose(ignored -> future.thenApply(writePacket(connection)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <K, V extends MinecraftPacket> CompletableFuture<WrappedPacket> hijackCurrentPacket(
|
|
||||||
MinecraftConnection connection,
|
|
||||||
CompletableFuture<WrappedPacket> binder,
|
|
||||||
CompletableFuture<K> future,
|
|
||||||
InstantPacketMapper<K, V> packetMapper
|
|
||||||
) {
|
|
||||||
CompletableFuture<WrappedPacket> awaitedFuture = new CompletableFuture<>();
|
|
||||||
// the binder will complete -> then the future will get the `write packet` caller
|
|
||||||
binder.whenComplete((previous, ignored) -> {
|
|
||||||
// map the new packet into a better "designed" packet with the hijacked packet's timestamp
|
|
||||||
WrappedPacket.wrap(previous.timestamp,
|
|
||||||
future.thenApply(item -> packetMapper.map(previous.timestamp, item)))
|
|
||||||
.thenApplyAsync(writePacket(connection), connection.eventLoop())
|
|
||||||
.whenComplete(
|
|
||||||
(packet, throwable) -> awaitedFuture.complete(throwable != null ? null : packet));
|
|
||||||
});
|
|
||||||
return awaitedFuture;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides an {@link Instant} based timestamp mapper from an existing object to create a packet.
|
* Tracks the last Secure Chat state that we received from the client. This is important to always have a valid 'last
|
||||||
|
* seen' state that is consistent with future and past updates from the client (which may be signed). This state is
|
||||||
|
* used to construct 'spoofed' command packets from the proxy to the server.
|
||||||
|
* <ul>
|
||||||
|
* <li>If we last forwarded a chat or command packet from the client, we have a known 'last seen' that we can
|
||||||
|
* reuse.</li>
|
||||||
|
* <li>If we last forwarded a {@link ChatAcknowledgementPacket}, the previous 'last seen' cannot be reused. We
|
||||||
|
* cannot predict an up-to-date 'last seen', as we do not know which messages the client actually saw.</li>
|
||||||
|
* <li>Therefore, we need to hold back any acknowledgement packets so that we can continue to reuse the last valid
|
||||||
|
* 'last seen' state.</li>
|
||||||
|
* <li>However, there is a limit to the number of messages that can remain unacknowledged on the server.</li>
|
||||||
|
* <li>To address this, we know that if the client has moved its 'last seen' window far enough, we can fill in the
|
||||||
|
* gap with dummy 'last seen', and it will never be checked.</li>
|
||||||
|
* </ul>
|
||||||
*
|
*
|
||||||
* @param <K> The base object type to map.
|
* Note that this is effectively unused for 1.20.5+ clients, as commands without any signature do not send 'last seen'
|
||||||
* @param <V> The resulting packet type.
|
* updates.
|
||||||
*/
|
*/
|
||||||
public interface InstantPacketMapper<K, V extends MinecraftPacket> {
|
public static class ChatState {
|
||||||
|
private static final int MINIMUM_DELAYED_ACK_COUNT = LastSeenMessages.WINDOW_SIZE;
|
||||||
|
private static final BitSet DUMMY_LAST_SEEN_MESSAGES = new BitSet();
|
||||||
|
|
||||||
/**
|
public volatile Instant lastTimestamp = Instant.EPOCH;
|
||||||
* Maps a value into a packet with it and a timestamp.
|
private volatile BitSet lastSeenMessages = new BitSet();
|
||||||
*
|
private final AtomicInteger delayedAckCount = new AtomicInteger();
|
||||||
* @param nextInstant the {@link Instant} timestamp to use for tracking.
|
|
||||||
* @param currentObject the current item to map to the packet.
|
|
||||||
* @return The resulting packet from the mapping.
|
|
||||||
*/
|
|
||||||
V map(Instant nextInstant, K currentObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class WrappedPacket {
|
private ChatState() {
|
||||||
|
|
||||||
private final Instant timestamp;
|
|
||||||
private final MinecraftPacket packet;
|
|
||||||
|
|
||||||
private WrappedPacket(Instant timestamp, MinecraftPacket packet) {
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
this.packet = packet;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public ChannelFuture write(MinecraftConnection connection) {
|
public LastSeenMessages updateFromMessage(@Nullable Instant timestamp, @Nullable LastSeenMessages lastSeenMessages) {
|
||||||
if (packet != null) {
|
if (timestamp != null) {
|
||||||
return connection.write(packet);
|
this.lastTimestamp = timestamp;
|
||||||
|
}
|
||||||
|
if (lastSeenMessages != null) {
|
||||||
|
// We held back some acknowledged messages, so flush that out now that we have a known 'last seen' state again
|
||||||
|
int delayedAckCount = this.delayedAckCount.getAndSet(0);
|
||||||
|
this.lastSeenMessages = lastSeenMessages.getAcknowledged();
|
||||||
|
return lastSeenMessages.offset(delayedAckCount);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CompletableFuture<WrappedPacket> wrap(Instant timestamp,
|
public int accumulateAckCount(int ackCount) {
|
||||||
CompletableFuture<MinecraftPacket> nextPacket) {
|
int delayedAckCount = this.delayedAckCount.addAndGet(ackCount);
|
||||||
return nextPacket
|
int ackCountToForward = delayedAckCount - MINIMUM_DELAYED_ACK_COUNT;
|
||||||
.thenApply(pkt -> new WrappedPacket(timestamp, pkt))
|
if (ackCountToForward >= LastSeenMessages.WINDOW_SIZE) {
|
||||||
.exceptionally(ignored -> new WrappedPacket(timestamp, null));
|
// Because we only forward acknowledgements above the window size, we don't have to shift the previous 'last seen' state
|
||||||
|
this.lastSeenMessages = DUMMY_LAST_SEEN_MESSAGES;
|
||||||
|
this.delayedAckCount.set(MINIMUM_DELAYED_ACK_COUNT);
|
||||||
|
return ackCountToForward;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LastSeenMessages createLastSeen() {
|
||||||
|
return new LastSeenMessages(0, lastSeenMessages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,11 +23,13 @@ import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
|||||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
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;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
|
||||||
public interface CommandHandler<T extends MinecraftPacket> {
|
public interface CommandHandler<T extends MinecraftPacket> {
|
||||||
|
|
||||||
@ -53,11 +55,12 @@ public interface CommandHandler<T extends MinecraftPacket> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
default void queueCommandResult(VelocityServer server, ConnectedPlayer player,
|
default void queueCommandResult(VelocityServer server, ConnectedPlayer player,
|
||||||
Function<CommandExecuteEvent, CompletableFuture<MinecraftPacket>> futurePacketCreator,
|
BiFunction<CommandExecuteEvent, LastSeenMessages, CompletableFuture<MinecraftPacket>> futurePacketCreator,
|
||||||
String message, Instant timestamp) {
|
String message, Instant timestamp, @Nullable LastSeenMessages lastSeenMessages) {
|
||||||
player.getChatQueue().queuePacket(
|
CompletableFuture<CommandExecuteEvent> eventFuture = server.getCommandManager().callCommandEvent(player, message);
|
||||||
server.getCommandManager().callCommandEvent(player, message)
|
player.getChatQueue().queuePacket(
|
||||||
.thenComposeAsync(futurePacketCreator)
|
newLastSeenMessages -> eventFuture
|
||||||
|
.thenComposeAsync(event -> futurePacketCreator.apply(event, newLastSeenMessages))
|
||||||
.thenApply(pkt -> {
|
.thenApply(pkt -> {
|
||||||
if (server.getConfiguration().isLogCommandExecutions()) {
|
if (server.getConfiguration().isLogCommandExecutions()) {
|
||||||
logger.info("{} -> executed command /{}", player, message);
|
logger.info("{} -> executed command /{}", player, message);
|
||||||
@ -68,6 +71,6 @@ public interface CommandHandler<T extends MinecraftPacket> {
|
|||||||
player.sendMessage(
|
player.sendMessage(
|
||||||
Component.translatable("velocity.command.generic-error", NamedTextColor.RED));
|
Component.translatable("velocity.command.generic-error", NamedTextColor.RED));
|
||||||
return null;
|
return null;
|
||||||
}), timestamp);
|
}), timestamp, lastSeenMessages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,8 @@ import java.util.BitSet;
|
|||||||
|
|
||||||
public class LastSeenMessages {
|
public class LastSeenMessages {
|
||||||
|
|
||||||
private static final int DIV_FLOOR = -Math.floorDiv(-20, 8);
|
public static final int WINDOW_SIZE = 20;
|
||||||
|
private static final int DIV_FLOOR = -Math.floorDiv(-WINDOW_SIZE, 8);
|
||||||
private int offset;
|
private int offset;
|
||||||
private BitSet acknowledged;
|
private BitSet acknowledged;
|
||||||
|
|
||||||
@ -33,6 +34,11 @@ public class LastSeenMessages {
|
|||||||
this.acknowledged = new BitSet();
|
this.acknowledged = new BitSet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LastSeenMessages(int offset, BitSet acknowledged) {
|
||||||
|
this.offset = offset;
|
||||||
|
this.acknowledged = acknowledged;
|
||||||
|
}
|
||||||
|
|
||||||
public LastSeenMessages(ByteBuf buf) {
|
public LastSeenMessages(ByteBuf buf) {
|
||||||
this.offset = ProtocolUtils.readVarInt(buf);
|
this.offset = ProtocolUtils.readVarInt(buf);
|
||||||
|
|
||||||
@ -46,14 +52,18 @@ public class LastSeenMessages {
|
|||||||
buf.writeBytes(Arrays.copyOf(acknowledged.toByteArray(), DIV_FLOOR));
|
buf.writeBytes(Arrays.copyOf(acknowledged.toByteArray(), DIV_FLOOR));
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEmpty() {
|
|
||||||
return acknowledged.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getOffset() {
|
public int getOffset() {
|
||||||
return this.offset;
|
return this.offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public BitSet getAcknowledged() {
|
||||||
|
return acknowledged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LastSeenMessages offset(final int offset) {
|
||||||
|
return new LastSeenMessages(this.offset + offset, acknowledged);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "LastSeenMessages{" +
|
return "LastSeenMessages{" +
|
||||||
|
@ -21,6 +21,7 @@ import com.velocitypowered.api.network.ProtocolVersion;
|
|||||||
import com.velocitypowered.api.proxy.Player;
|
import com.velocitypowered.api.proxy.Player;
|
||||||
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.ChatType;
|
import com.velocitypowered.proxy.protocol.packet.chat.ChatType;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import net.kyori.adventure.identity.Identity;
|
import net.kyori.adventure.identity.Identity;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
@ -36,6 +37,7 @@ public abstract class ChatBuilderV2 {
|
|||||||
protected @Nullable Identity senderIdentity;
|
protected @Nullable Identity senderIdentity;
|
||||||
protected Instant timestamp;
|
protected Instant timestamp;
|
||||||
protected ChatType type = ChatType.CHAT;
|
protected ChatType type = ChatType.CHAT;
|
||||||
|
protected @Nullable LastSeenMessages lastSeenMessages;
|
||||||
|
|
||||||
protected ChatBuilderV2(ProtocolVersion version) {
|
protected ChatBuilderV2(ProtocolVersion version) {
|
||||||
this.version = version;
|
this.version = version;
|
||||||
@ -77,6 +79,11 @@ public abstract class ChatBuilderV2 {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ChatBuilderV2 setLastSeenMessages(LastSeenMessages lastSeenMessages) {
|
||||||
|
this.lastSeenMessages = lastSeenMessages;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public abstract MinecraftPacket toClient();
|
public abstract MinecraftPacket toClient();
|
||||||
|
|
||||||
public abstract MinecraftPacket toServer();
|
public abstract MinecraftPacket toServer();
|
||||||
|
@ -91,11 +91,12 @@ public class KeyedChatHandler implements
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
chatQueue.queuePacket(
|
chatQueue.queuePacket(
|
||||||
chatFuture.exceptionally((ex) -> {
|
newLastSeen -> chatFuture.exceptionally((ex) -> {
|
||||||
logger.error("Exception while handling player chat for {}", player, ex);
|
logger.error("Exception while handling player chat for {}", player, ex);
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
packet.getExpiry()
|
packet.getExpiry(),
|
||||||
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ public class KeyedCommandHandler implements CommandHandler<KeyedPlayerCommandPac
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handlePlayerCommandInternal(KeyedPlayerCommandPacket packet) {
|
public void handlePlayerCommandInternal(KeyedPlayerCommandPacket packet) {
|
||||||
queueCommandResult(this.server, this.player, event -> {
|
queueCommandResult(this.server, this.player, (event, newLastSeenMessages) -> {
|
||||||
CommandExecuteEvent.CommandResult result = event.getResult();
|
CommandExecuteEvent.CommandResult result = event.getResult();
|
||||||
IdentifiedKey playerKey = player.getIdentifiedKey();
|
IdentifiedKey playerKey = player.getIdentifiedKey();
|
||||||
if (result == CommandExecuteEvent.CommandResult.denied()) {
|
if (result == CommandExecuteEvent.CommandResult.denied()) {
|
||||||
@ -111,6 +111,6 @@ public class KeyedCommandHandler implements CommandHandler<KeyedPlayerCommandPac
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}, packet.getCommand(), packet.getTimestamp());
|
}, packet.getCommand(), packet.getTimestamp(), null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ public class LegacyCommandHandler implements CommandHandler<LegacyChatPacket> {
|
|||||||
@Override
|
@Override
|
||||||
public void handlePlayerCommandInternal(LegacyChatPacket packet) {
|
public void handlePlayerCommandInternal(LegacyChatPacket packet) {
|
||||||
String command = packet.getMessage().substring(1);
|
String command = packet.getMessage().substring(1);
|
||||||
queueCommandResult(this.server, this.player, event -> {
|
queueCommandResult(this.server, this.player, (event, newLastSeenMessages) -> {
|
||||||
CommandExecuteEvent.CommandResult result = event.getResult();
|
CommandExecuteEvent.CommandResult result = event.getResult();
|
||||||
if (result == CommandExecuteEvent.CommandResult.denied()) {
|
if (result == CommandExecuteEvent.CommandResult.denied()) {
|
||||||
return CompletableFuture.completedFuture(null);
|
return CompletableFuture.completedFuture(null);
|
||||||
@ -62,6 +62,6 @@ public class LegacyCommandHandler implements CommandHandler<LegacyChatPacket> {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}, command, Instant.now());
|
}, command, Instant.now(), null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ public class SessionChatBuilder extends ChatBuilderV2 {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MinecraftPacket toServer() {
|
public MinecraftPacket toServer() {
|
||||||
|
LastSeenMessages lastSeenMessages = this.lastSeenMessages != null ? this.lastSeenMessages : new LastSeenMessages();
|
||||||
if (message.startsWith("/")) {
|
if (message.startsWith("/")) {
|
||||||
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) {
|
if (version.noLessThan(ProtocolVersion.MINECRAFT_1_20_5)) {
|
||||||
UnsignedPlayerCommandPacket command = new UnsignedPlayerCommandPacket();
|
UnsignedPlayerCommandPacket command = new UnsignedPlayerCommandPacket();
|
||||||
@ -52,7 +53,7 @@ public class SessionChatBuilder extends ChatBuilderV2 {
|
|||||||
command.salt = 0L;
|
command.salt = 0L;
|
||||||
command.timeStamp = timestamp;
|
command.timeStamp = timestamp;
|
||||||
command.argumentSignatures = new SessionPlayerCommandPacket.ArgumentSignatures();
|
command.argumentSignatures = new SessionPlayerCommandPacket.ArgumentSignatures();
|
||||||
command.lastSeenMessages = new LastSeenMessages();
|
command.lastSeenMessages = lastSeenMessages;
|
||||||
return command;
|
return command;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -62,7 +63,7 @@ public class SessionChatBuilder extends ChatBuilderV2 {
|
|||||||
chat.signature = new byte[0];
|
chat.signature = new byte[0];
|
||||||
chat.timestamp = timestamp;
|
chat.timestamp = timestamp;
|
||||||
chat.salt = 0L;
|
chat.salt = 0L;
|
||||||
chat.lastSeenMessages = new LastSeenMessages();
|
chat.lastSeenMessages = lastSeenMessages;
|
||||||
return chat;
|
return chat;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,8 @@ import com.velocitypowered.proxy.protocol.packet.chat.ChatQueue;
|
|||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
public class SessionChatHandler implements ChatHandler<SessionPlayerChatPacket> {
|
public class SessionChatHandler implements ChatHandler<SessionPlayerChatPacket> {
|
||||||
|
|
||||||
private static final Logger logger = LogManager.getLogger(SessionChatHandler.class);
|
private static final Logger logger = LogManager.getLogger(SessionChatHandler.class);
|
||||||
@ -51,8 +53,9 @@ public class SessionChatHandler implements ChatHandler<SessionPlayerChatPacket>
|
|||||||
ChatQueue chatQueue = this.player.getChatQueue();
|
ChatQueue chatQueue = this.player.getChatQueue();
|
||||||
EventManager eventManager = this.server.getEventManager();
|
EventManager eventManager = this.server.getEventManager();
|
||||||
PlayerChatEvent toSend = new PlayerChatEvent(player, packet.getMessage());
|
PlayerChatEvent toSend = new PlayerChatEvent(player, packet.getMessage());
|
||||||
|
CompletableFuture<PlayerChatEvent> eventFuture = eventManager.fire(toSend);
|
||||||
chatQueue.queuePacket(
|
chatQueue.queuePacket(
|
||||||
eventManager.fire(toSend)
|
newLastSeenMessages -> eventFuture
|
||||||
.thenApply(pme -> {
|
.thenApply(pme -> {
|
||||||
PlayerChatEvent.ChatResult chatResult = pme.getResult();
|
PlayerChatEvent.ChatResult chatResult = pme.getResult();
|
||||||
if (!chatResult.isAllowed()) {
|
if (!chatResult.isAllowed()) {
|
||||||
@ -70,15 +73,17 @@ public class SessionChatHandler implements ChatHandler<SessionPlayerChatPacket>
|
|||||||
}
|
}
|
||||||
return this.player.getChatBuilderFactory().builder().message(packet.message)
|
return this.player.getChatBuilderFactory().builder().message(packet.message)
|
||||||
.setTimestamp(packet.timestamp)
|
.setTimestamp(packet.timestamp)
|
||||||
|
.setLastSeenMessages(newLastSeenMessages)
|
||||||
.toServer();
|
.toServer();
|
||||||
}
|
}
|
||||||
return packet;
|
return packet.withLastSeenMessages(newLastSeenMessages);
|
||||||
})
|
})
|
||||||
.exceptionally((ex) -> {
|
.exceptionally((ex) -> {
|
||||||
logger.error("Exception while handling player chat for {}", player, ex);
|
logger.error("Exception while handling player chat for {}", player, ex);
|
||||||
return null;
|
return null;
|
||||||
}),
|
}),
|
||||||
packet.getTimestamp()
|
packet.getTimestamp(),
|
||||||
|
packet.getLastSeenMessages()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,13 +18,14 @@
|
|||||||
package com.velocitypowered.proxy.protocol.packet.chat.session;
|
package com.velocitypowered.proxy.protocol.packet.chat.session;
|
||||||
|
|
||||||
import com.velocitypowered.api.event.command.CommandExecuteEvent;
|
import com.velocitypowered.api.event.command.CommandExecuteEvent;
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
import com.velocitypowered.proxy.connection.client.ConnectedPlayer;
|
||||||
|
import com.velocitypowered.proxy.protocol.MinecraftPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket;
|
import com.velocitypowered.proxy.protocol.packet.chat.ChatAcknowledgementPacket;
|
||||||
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
|
import com.velocitypowered.proxy.protocol.packet.chat.CommandHandler;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import net.kyori.adventure.text.Component;
|
import net.kyori.adventure.text.Component;
|
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
|
||||||
public class SessionCommandHandler implements CommandHandler<SessionPlayerCommandPacket> {
|
public class SessionCommandHandler implements CommandHandler<SessionPlayerCommandPacket> {
|
||||||
|
|
||||||
@ -41,78 +42,81 @@ public class SessionCommandHandler implements CommandHandler<SessionPlayerComman
|
|||||||
return SessionPlayerCommandPacket.class;
|
return SessionPlayerCommandPacket.class;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private MinecraftPacket consumeCommand(SessionPlayerCommandPacket packet) {
|
||||||
|
if (packet.lastSeenMessages == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (packet.isSigned()) {
|
||||||
|
// Any signed message produced by the client *must* be passed through to the server in order to maintain a
|
||||||
|
// consistent state for future messages.
|
||||||
|
logger.fatal("A plugin tried to deny a command with signable component(s). "
|
||||||
|
+ "This is not supported. "
|
||||||
|
+ "Disconnecting player " + player.getUsername() + ". Command packet: " + packet);
|
||||||
|
player.disconnect(Component.text(
|
||||||
|
"A proxy plugin caused an illegal protocol state. "
|
||||||
|
+ "Contact your network administrator."));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// An unsigned command with a 'last seen' update will not happen as of 1.20.5+, but for earlier versions - we still
|
||||||
|
// need to pass through the acknowledgement
|
||||||
|
final int offset = packet.lastSeenMessages.getOffset();
|
||||||
|
if (offset != 0) {
|
||||||
|
return new ChatAcknowledgementPacket(offset);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private MinecraftPacket forwardCommand(SessionPlayerCommandPacket packet, String newCommand) {
|
||||||
|
if (newCommand.equals(packet.command)) {
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
return modifyCommand(packet, newCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private MinecraftPacket modifyCommand(SessionPlayerCommandPacket packet, String newCommand) {
|
||||||
|
if (packet.isSigned()) {
|
||||||
|
logger.fatal("A plugin tried to change a command with signed component(s). "
|
||||||
|
+ "This is not supported. "
|
||||||
|
+ "Disconnecting player " + player.getUsername() + ". Command packet: " + packet);
|
||||||
|
player.disconnect(Component.text(
|
||||||
|
"A proxy plugin caused an illegal protocol state. "
|
||||||
|
+ "Contact your network administrator."));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.player.getChatBuilderFactory()
|
||||||
|
.builder()
|
||||||
|
.setTimestamp(packet.timeStamp)
|
||||||
|
.setLastSeenMessages(packet.lastSeenMessages)
|
||||||
|
.asPlayer(this.player)
|
||||||
|
.message("/" + newCommand)
|
||||||
|
.toServer();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handlePlayerCommandInternal(SessionPlayerCommandPacket packet) {
|
public void handlePlayerCommandInternal(SessionPlayerCommandPacket packet) {
|
||||||
queueCommandResult(this.server, this.player, event -> {
|
queueCommandResult(this.server, this.player, (event, newLastSeenMessages) -> {
|
||||||
|
SessionPlayerCommandPacket fixedPacket = packet.withLastSeenMessages(newLastSeenMessages);
|
||||||
|
|
||||||
CommandExecuteEvent.CommandResult result = event.getResult();
|
CommandExecuteEvent.CommandResult result = event.getResult();
|
||||||
if (result == CommandExecuteEvent.CommandResult.denied()) {
|
if (result == CommandExecuteEvent.CommandResult.denied()) {
|
||||||
if (packet.isSigned()) {
|
return CompletableFuture.completedFuture(consumeCommand(fixedPacket));
|
||||||
logger.fatal("A plugin tried to deny a command with signable component(s). "
|
|
||||||
+ "This is not supported. "
|
|
||||||
+ "Disconnecting player " + player.getUsername() + ". Command packet: " + packet);
|
|
||||||
player.disconnect(Component.text(
|
|
||||||
"A proxy plugin caused an illegal protocol state. "
|
|
||||||
+ "Contact your network administrator."));
|
|
||||||
}
|
|
||||||
// We seemingly can't actually do this if signed args exist, if not, we can probs keep stuff happy
|
|
||||||
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3) && packet.lastSeenMessages != null) {
|
|
||||||
return CompletableFuture.completedFuture(new ChatAcknowledgementPacket(packet.lastSeenMessages.getOffset()));
|
|
||||||
}
|
|
||||||
return CompletableFuture.completedFuture(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String commandToRun = result.getCommand().orElse(packet.command);
|
String commandToRun = result.getCommand().orElse(fixedPacket.command);
|
||||||
if (result.isForwardToServer()) {
|
if (result.isForwardToServer()) {
|
||||||
if (packet.isSigned() && commandToRun.equals(packet.command)) {
|
return CompletableFuture.completedFuture(forwardCommand(fixedPacket, commandToRun));
|
||||||
return CompletableFuture.completedFuture(packet);
|
|
||||||
} else {
|
|
||||||
if (packet.isSigned()) {
|
|
||||||
logger.fatal("A plugin tried to change a command with signed component(s). "
|
|
||||||
+ "This is not supported. "
|
|
||||||
+ "Disconnecting player " + player.getUsername() + ". Command packet: " + packet);
|
|
||||||
player.disconnect(Component.text(
|
|
||||||
"A proxy plugin caused an illegal protocol state. "
|
|
||||||
+ "Contact your network administrator."));
|
|
||||||
return CompletableFuture.completedFuture(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
return CompletableFuture.completedFuture(this.player.getChatBuilderFactory()
|
|
||||||
.builder()
|
|
||||||
.setTimestamp(packet.timeStamp)
|
|
||||||
.asPlayer(this.player)
|
|
||||||
.message("/" + commandToRun)
|
|
||||||
.toServer());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return runCommand(this.server, this.player, commandToRun, hasRun -> {
|
return runCommand(this.server, this.player, commandToRun, hasRun -> {
|
||||||
if (!hasRun) {
|
if (hasRun) {
|
||||||
if (packet.isSigned() && commandToRun.equals(packet.command)) {
|
return consumeCommand(fixedPacket);
|
||||||
return packet;
|
|
||||||
} else {
|
|
||||||
if (packet.isSigned()) {
|
|
||||||
logger.fatal("A plugin tried to change a command with signed component(s). "
|
|
||||||
+ "This is not supported. "
|
|
||||||
+ "Disconnecting player " + player.getUsername() + ". Command packet: " + packet);
|
|
||||||
player.disconnect(Component.text(
|
|
||||||
"A proxy plugin caused an illegal protocol state. "
|
|
||||||
+ "Contact your network administrator."));
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.player.getChatBuilderFactory()
|
|
||||||
.builder()
|
|
||||||
.setTimestamp(packet.timeStamp)
|
|
||||||
.asPlayer(this.player)
|
|
||||||
.message("/" + commandToRun)
|
|
||||||
.toServer();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (player.getProtocolVersion().noLessThan(ProtocolVersion.MINECRAFT_1_19_3) && packet.lastSeenMessages != null) {
|
return forwardCommand(fixedPacket, commandToRun);
|
||||||
return new ChatAcknowledgementPacket(packet.lastSeenMessages.getOffset());
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
});
|
||||||
}, packet.command, packet.timeStamp);
|
}, packet.command, packet.timeStamp, packet.lastSeenMessages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,4 +100,15 @@ public class SessionPlayerChatPacket implements MinecraftPacket {
|
|||||||
buf.readBytes(signature);
|
buf.readBytes(signature);
|
||||||
return signature;
|
return signature;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SessionPlayerChatPacket withLastSeenMessages(LastSeenMessages lastSeenMessages) {
|
||||||
|
SessionPlayerChatPacket packet = new SessionPlayerChatPacket();
|
||||||
|
packet.message = message;
|
||||||
|
packet.timestamp = timestamp;
|
||||||
|
packet.salt = salt;
|
||||||
|
packet.signed = signed;
|
||||||
|
packet.signature = signature;
|
||||||
|
packet.lastSeenMessages = lastSeenMessages;
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,8 @@ import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
|||||||
import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages;
|
import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages;
|
||||||
import com.velocitypowered.proxy.util.except.QuietDecoderException;
|
import com.velocitypowered.proxy.util.except.QuietDecoderException;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -63,8 +65,7 @@ public class SessionPlayerCommandPacket implements MinecraftPacket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSigned() {
|
public boolean isSigned() {
|
||||||
if (salt == 0) return false;
|
return !argumentSignatures.isEmpty();
|
||||||
return !lastSeenMessages.isEmpty() || !argumentSignatures.isEmpty();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -83,6 +84,21 @@ public class SessionPlayerCommandPacket implements MinecraftPacket {
|
|||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SessionPlayerCommandPacket withLastSeenMessages(@Nullable LastSeenMessages lastSeenMessages) {
|
||||||
|
if (lastSeenMessages == null) {
|
||||||
|
UnsignedPlayerCommandPacket packet = new UnsignedPlayerCommandPacket();
|
||||||
|
packet.command = command;
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
SessionPlayerCommandPacket packet = new SessionPlayerCommandPacket();
|
||||||
|
packet.command = command;
|
||||||
|
packet.timeStamp = timeStamp;
|
||||||
|
packet.salt = salt;
|
||||||
|
packet.argumentSignatures = argumentSignatures;
|
||||||
|
packet.lastSeenMessages = lastSeenMessages;
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
public static class ArgumentSignatures {
|
public static class ArgumentSignatures {
|
||||||
|
|
||||||
private final List<ArgumentSignature> entries;
|
private final List<ArgumentSignature> entries;
|
||||||
|
@ -19,7 +19,9 @@ package com.velocitypowered.proxy.protocol.packet.chat.session;
|
|||||||
|
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
import com.velocitypowered.proxy.protocol.ProtocolUtils;
|
||||||
|
import com.velocitypowered.proxy.protocol.packet.chat.LastSeenMessages;
|
||||||
import io.netty.buffer.ByteBuf;
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||||
|
|
||||||
public class UnsignedPlayerCommandPacket extends SessionPlayerCommandPacket {
|
public class UnsignedPlayerCommandPacket extends SessionPlayerCommandPacket {
|
||||||
|
|
||||||
@ -33,6 +35,11 @@ public class UnsignedPlayerCommandPacket extends SessionPlayerCommandPacket {
|
|||||||
ProtocolUtils.writeString(buf, this.command);
|
ProtocolUtils.writeString(buf, this.command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SessionPlayerCommandPacket withLastSeenMessages(@Nullable LastSeenMessages lastSeenMessages) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isSigned() {
|
public boolean isSigned() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
package com.velocitypowered.proxy.protocol.packet.config;
|
package com.velocitypowered.proxy.protocol.packet.config;
|
||||||
|
|
||||||
import com.velocitypowered.api.network.ProtocolVersion;
|
import com.velocitypowered.api.network.ProtocolVersion;
|
||||||
|
import com.velocitypowered.api.util.ServerLink;
|
||||||
import com.velocitypowered.proxy.connection.MinecraftSessionHandler;
|
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;
|
||||||
@ -66,6 +67,13 @@ public class ClientboundServerLinksPacket implements MinecraftPacket {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public record ServerLink(int id, ComponentHolder displayName, String url) {
|
public record ServerLink(int id, ComponentHolder displayName, String url) {
|
||||||
|
|
||||||
|
public ServerLink(com.velocitypowered.api.util.ServerLink link, ProtocolVersion protocolVersion) {
|
||||||
|
this(link.getBuiltInType().map(Enum::ordinal).orElse(-1),
|
||||||
|
link.getCustomLabel().map(c -> new ComponentHolder(protocolVersion, c)).orElse(null),
|
||||||
|
link.getUrl().toString());
|
||||||
|
}
|
||||||
|
|
||||||
private static ServerLink read(ByteBuf buf, ProtocolVersion version) {
|
private static ServerLink read(ByteBuf buf, ProtocolVersion version) {
|
||||||
if (buf.readBoolean()) {
|
if (buf.readBoolean()) {
|
||||||
return new ServerLink(ProtocolUtils.readVarInt(buf), null, ProtocolUtils.readString(buf));
|
return new ServerLink(ProtocolUtils.readVarInt(buf), null, ProtocolUtils.readString(buf));
|
||||||
|
@ -19,6 +19,8 @@ package com.velocitypowered.proxy.server;
|
|||||||
|
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.velocitypowered.api.event.proxy.server.ServerRegisteredEvent;
|
||||||
|
import com.velocitypowered.api.event.proxy.server.ServerUnregisteredEvent;
|
||||||
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
import com.velocitypowered.api.proxy.server.RegisteredServer;
|
||||||
import com.velocitypowered.api.proxy.server.ServerInfo;
|
import com.velocitypowered.api.proxy.server.ServerInfo;
|
||||||
import com.velocitypowered.proxy.VelocityServer;
|
import com.velocitypowered.proxy.VelocityServer;
|
||||||
@ -84,6 +86,10 @@ public class ServerMap {
|
|||||||
throw new IllegalArgumentException(
|
throw new IllegalArgumentException(
|
||||||
"Server with name " + serverInfo.getName() + " already registered");
|
"Server with name " + serverInfo.getName() + " already registered");
|
||||||
} else if (existing == null) {
|
} else if (existing == null) {
|
||||||
|
if (server != null) {
|
||||||
|
server.getEventManager().fireAndForget(new ServerRegisteredEvent(rs));
|
||||||
|
}
|
||||||
|
|
||||||
return rs;
|
return rs;
|
||||||
} else {
|
} else {
|
||||||
return existing;
|
return existing;
|
||||||
@ -107,5 +113,9 @@ public class ServerMap {
|
|||||||
"Trying to remove server %s with differing information", serverInfo.getName());
|
"Trying to remove server %s with differing information", serverInfo.getName());
|
||||||
Preconditions.checkState(servers.remove(lowerName, rs),
|
Preconditions.checkState(servers.remove(lowerName, rs),
|
||||||
"Server with name %s replaced whilst unregistering", serverInfo.getName());
|
"Server with name %s replaced whilst unregistering", serverInfo.getName());
|
||||||
|
|
||||||
|
if (server != null) {
|
||||||
|
server.getEventManager().fireAndForget(new ServerUnregisteredEvent(rs));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# Config version. Do not change this
|
# Config version. Do not change this
|
||||||
config-version = "2.7"
|
config-version = "2.7"
|
||||||
|
|
||||||
# What port should the proxy be bound to? By default, we'll bind to all addresses on port 25577.
|
# What port should the proxy be bound to? By default, we'll bind to all addresses on port 25565.
|
||||||
bind = "0.0.0.0:25577"
|
bind = "0.0.0.0:25565"
|
||||||
|
|
||||||
# What should be the MOTD? This gets displayed when the player adds your server to
|
# What should be the MOTD? This gets displayed when the player adds your server to
|
||||||
# their server list. Only MiniMessage format is accepted.
|
# their server list. Only MiniMessage format is accepted.
|
||||||
@ -150,7 +150,7 @@ accepts-transfers = false
|
|||||||
enabled = false
|
enabled = false
|
||||||
|
|
||||||
# If query is enabled, on what port should the query protocol listen on?
|
# If query is enabled, on what port should the query protocol listen on?
|
||||||
port = 25577
|
port = 25565
|
||||||
|
|
||||||
# This is the map name that is reported to the query services.
|
# This is the map name that is reported to the query services.
|
||||||
map = "Velocity"
|
map = "Velocity"
|
||||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren