Skip to content
Merged
9 changes: 6 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ java {

repositories {
mavenLocal()
mavenCentral()
maven { url = uri("https://maven-central.storage-download.googleapis.com/maven2") }
mavenCentral {
content { excludeGroupByRegex("com\\.github\\..*") }
}
maven { url = uri("https://jitpack.io") }
}

Expand All @@ -39,9 +42,9 @@ dependencies {
compileOnly("com.github.EaseCation:authlib-stub:master-SNAPSHOT")

// 注解处理和编译时依赖
compileOnly("org.projectlombok:lombok:1.18.38")
compileOnly(libs.lombok)
compileOnly("com.google.code.findbugs:jsr305:3.0.2")
annotationProcessor("org.projectlombok:lombok:1.18.38")
annotationProcessor(libs.lombok)

// 测试依赖
testImplementation(libs.nukkit)
Expand Down
3 changes: 2 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[versions]
nukkit = "324e8625f2fc87fdb85cf0d8f9c5364a2690b401"
nukkit = "928094afacb375d65d6294d1f445824a89206423"

[libraries]
nukkit = { module = "com.github.EaseCation:Nukkit", version.ref = "nukkit" }
lombok = { module = "org.projectlombok:lombok", version = "1.18.46" }
7 changes: 6 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ rootProject.name = "SynapseAPI"

pluginManagement {
repositories {
// JitPack出口访问Maven Central可能被限流,优先使用其Google只读镜像。
maven { url = uri("https://maven-central.storage-download.googleapis.com/maven2") }
gradlePluginPortal()
}
}
Expand All @@ -13,7 +15,10 @@ plugins {
dependencyResolutionManagement {
repositories {
mavenLocal()
mavenCentral()
maven { url = uri("https://maven-central.storage-download.googleapis.com/maven2") }
mavenCentral {
content { excludeGroupByRegex("com\\.github\\..*") }
}
maven { url = uri("https://jitpack.io") }
}
}
39 changes: 39 additions & 0 deletions src/main/java/org/itxtech/synapseapi/AttackAttempts.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.itxtech.synapseapi;

/** 同一真实服务器 tick 的所有输入阶段共享原攻击尝试额度。 */
final class AttackAttempts {
private final int rejectAt;
private int tick = Integer.MIN_VALUE;
private int count;

AttackAttempts(int rejectAt) {
if (rejectAt <= 1) {
throw new IllegalArgumentException("Attack rejection threshold must exceed one");
}
this.rejectAt = rejectAt;
}

boolean claim(int serverTick) {
this.rotate(serverTick);
// 到达拒绝值后保持拒绝,不能让无效流量把计数器溢出后重新放行。
if (this.count < this.rejectAt) {
this.count++;
}
return this.count < this.rejectAt;
}

private void rotate(int serverTick) {
if (this.tick != serverTick) {
this.tick = serverTick;
this.count = 0;
}
}

int tick() {
return this.tick;
}

int count(int serverTick) {
return this.tick == serverTick ? this.count : 0;
}
}
177 changes: 177 additions & 0 deletions src/main/java/org/itxtech/synapseapi/BetweenTickPackets.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package org.itxtech.synapseapi;

import cn.nukkit.network.protocol.DataPacket;
import cn.nukkit.network.protocol.AnimatePacket;
import cn.nukkit.network.protocol.MobEquipmentPacket;
import cn.nukkit.network.protocol.PlayerHotbarPacket;
import cn.nukkit.network.protocol.types.ContainerIds;
import cn.nukkit.network.protocol.types.NetworkInventoryAction;
import cn.nukkit.inventory.transaction.data.ReleaseItemData;
import cn.nukkit.inventory.transaction.data.UseItemData;
import cn.nukkit.inventory.transaction.data.UseItemOnEntityData;
import org.itxtech.synapseapi.multiprotocol.common.PlayerAuthInputFlags;
import org.itxtech.synapseapi.multiprotocol.protocol113.protocol.IPlayerAuthInputPacket;
import org.itxtech.synapseapi.multiprotocol.protocol19.protocol.NetworkStackLatencyPacket19;
import org.itxtech.synapseapi.multiprotocol.protocol14.protocol.PlayerHotbarPacket14;
import org.itxtech.synapseapi.multiprotocol.protocol116.protocol.InventoryTransactionPacket116;
import org.itxtech.synapseapi.multiprotocol.protocol121120.protocol.AnimatePacket121120;
import org.itxtech.synapseapi.multiprotocol.protocol121130.protocol.AnimatePacket121130;
import org.itxtech.synapseapi.multiprotocol.protocol16.protocol.NEPyRpcPacket16;

import java.util.BitSet;

/** 仅分类已审计的协议工作,不从客户端状态推导服务端物理或权限。 */
final class BetweenTickPackets {
private static final long MOVEMENT_FLAGS = ((1L << PlayerAuthInputFlags.START_GLIDING) - 1)
& ~((1L << PlayerAuthInputFlags.START_SWIMMING) | (1L << PlayerAuthInputFlags.STOP_SWIMMING))
| (1L << PlayerAuthInputFlags.HANDLED_TELEPORT);
private static final long CONTEXT_FLAGS = (1L << PlayerAuthInputFlags.ACK_ENTITY_DATA)
| (1L << PlayerAuthInputFlags.BLOCK_BREAKING_DELAY_ENABLED)
| (1L << PlayerAuthInputFlags.HORIZONTAL_COLLISION)
| (1L << PlayerAuthInputFlags.VERTICAL_COLLISION)
| (1L << PlayerAuthInputFlags.DOWN_LEFT)
| (1L << PlayerAuthInputFlags.DOWN_RIGHT)
| (1L << PlayerAuthInputFlags.IS_CAMERA_RELATIVE_MOVEMENT_ENABLED)
| (1L << PlayerAuthInputFlags.IS_ROT_CONTROLLED_BY_MOVE_DIRECTION)
| (1L << PlayerAuthInputFlags.IS_HOTBAR_ONLY_TOUCH)
| (1L << PlayerAuthInputFlags.JUMP_RELEASED_RAW)
| (1L << PlayerAuthInputFlags.JUMP_PRESSED_RAW)
| (1L << PlayerAuthInputFlags.JUMP_CURRENT_RAW)
| (1L << PlayerAuthInputFlags.SNEAK_RELEASED_RAW)
| (1L << PlayerAuthInputFlags.SNEAK_PRESSED_RAW);
private static final long CONTEXT_FLAGS2 = 1L << (PlayerAuthInputFlags.SNEAK_CURRENT_RAW - Long.SIZE);

private BetweenTickPackets() {
}

static boolean supports(DataPacket packet) {
if (packet instanceof NetworkStackLatencyPacket19) {
return true;
}
if (packet instanceof NEPyRpcPacket16 rpc) {
// JE 的输入扩展先于普通交互包;解码限额及每个回调后的会话复检沿用原实现。
return rpc.subPackets != null;
}
if (packet instanceof MobEquipmentPacket equipment) {
return equipment.windowId == ContainerIds.INVENTORY && equipment.item != null;
}
if (packet instanceof PlayerHotbarPacket hotbar) {
return hotbar.windowId == ContainerIds.INVENTORY;
}
if (packet instanceof PlayerHotbarPacket14 hotbar) {
return hotbar.windowId == ContainerIds.INVENTORY;
}
if (packet instanceof AnimatePacket animation) {
return animation.action == AnimatePacket.Action.SWING_ARM;
}
if (packet instanceof AnimatePacket121120 animation) {
return animation.action == AnimatePacket.Action.SWING_ARM && Float.isFinite(animation.data);
}
if (packet instanceof AnimatePacket121130 animation) {
return animation.action == AnimatePacket.Action.SWING_ARM && Float.isFinite(animation.data);
}
if (packet instanceof InventoryTransactionPacket116 transaction) {
return supportsItemAction(transaction);
}
if (!(packet instanceof IPlayerAuthInputPacket input)) {
return false;
}
UseItemData use = input.getUseItemData();
boolean embeddedUse = use != null;
if (input.hasFlag(PlayerAuthInputFlags.PERFORM_ITEM_INTERACTION) != embeddedUse
|| input.getItemStackRequest() != null || input.isCraftingPart()
|| input.isEnchantingPart() || input.isRepairItemPart()
|| input.getBlockActions() != null && input.getBlockActions().length != 0) {
return false;
}
if (embeddedUse) {
// 仍由原 AuthInput handler 在有效移动提交后完整处理,不拆分内嵌动作。
if (use.actionType != InventoryTransactionPacket116.USE_ITEM_ACTION_CLICK_AIR
|| use.itemInHand == null || !supportsInventoryActions(input.getInventoryActions())) {
return false;
}
} else if (input.getLegacyRequestId() != 0
|| input.getInventoryActions() != null && input.getInventoryActions().length != 0
|| input.getRequestChangedSlots() != null && input.getRequestChangedSlots().length != 0) {
return false;
}
int shift = input.getNeteaseFlagsVersion();
// 当前解码器只实现原版、一个和两个附加位的三种布局。
if (shift < 0 || shift > 2) {
return false;
}
long formatFlags = ((1L << shift) - 1) << PlayerAuthInputFlags.ACK_ENTITY_DATA;
long allowed = MOVEMENT_FLAGS | (1L << PlayerAuthInputFlags.MISSED_SWING) | formatFlags | (CONTEXT_FLAGS << shift)
| (embeddedUse ? 1L << PlayerAuthInputFlags.PERFORM_ITEM_INTERACTION : 0);
long allowed2 = (CONTEXT_FLAGS2 << shift)
| (shift == 0 ? 0 : CONTEXT_FLAGS >>> (Long.SIZE - shift));
BitSet flags = input.getInputFlags();
if (flags == null) return false;
for (int bit = flags.nextSetBit(0); bit >= 0; bit = flags.nextSetBit(bit + 1)) {
if (bit >= Long.SIZE * 2 || ((bit < Long.SIZE ? allowed : allowed2) & (1L << bit)) == 0) {
return false;
}
}
return true;
}

/** 按工作职责读取当前状态;切槽和释放不能被同一玩家的持续使用状态锁住。 */
static boolean canRunBetweenTicks(SynapsePlayer player, DataPacket packet) {
if (packet instanceof NetworkStackLatencyPacket19) {
return true;
}
if (packet instanceof InventoryTransactionPacket116) {
return player.canProcessItemActionBetweenTicks();
}
if (packet instanceof IPlayerAuthInputPacket input) {
return player.canProcessMovementBetweenTicks()
// 挥空音效沿原处理器执行;开启声音时也需要当前世界动作资格。
&& (input.getUseItemData() == null
&& (!input.hasFlag(PlayerAuthInputFlags.MISSED_SWING) || !player.isServerAuthoritativeSoundEnabled())
|| player.canProcessItemActionBetweenTicks());
}
return player.canProcessInputStateBetweenTicks();
}

private static boolean supportsItemAction(InventoryTransactionPacket116 packet) {
if (packet.isCraftingPart || packet.isEnchantingPart || packet.isRepairItemPart
|| packet.actions == null) {
return false;
}
// 旧版槽位同步元数据不参与原处理器的库存结算,其编号也不是窗口编号。
// 保留完整元数据,准入只依据下方真正的库存动作;不能用同步声明替代动作校验。
// 完整消费原包,普通容器、合成与创造动作仍保留为队头屏障。
if (!supportsInventoryActions(packet.actions)) {
return false;
}
return switch (packet.transactionType) {
case InventoryTransactionPacket116.TYPE_USE_ITEM_ON_ENTITY ->
packet.transactionData instanceof UseItemOnEntityData data
&& data.actionType == InventoryTransactionPacket116.USE_ITEM_ON_ENTITY_ACTION_ATTACK
&& data.itemInHand != null;
case InventoryTransactionPacket116.TYPE_USE_ITEM ->
packet.transactionData instanceof UseItemData data
&& data.actionType == InventoryTransactionPacket116.USE_ITEM_ACTION_CLICK_AIR
&& data.itemInHand != null;
case InventoryTransactionPacket116.TYPE_RELEASE_ITEM ->
packet.transactionData instanceof ReleaseItemData data
&& (data.actionType == InventoryTransactionPacket116.RELEASE_ITEM_ACTION_RELEASE
|| data.actionType == InventoryTransactionPacket116.RELEASE_ITEM_ACTION_CONSUME)
&& data.itemInHand != null;
default -> false;
};
}

private static boolean supportsInventoryActions(NetworkInventoryAction[] actions) {
if (actions == null) {
return false;
}
for (NetworkInventoryAction action : actions) {
if (action == null || action.sourceType != NetworkInventoryAction.SOURCE_CONTAINER
|| action.windowId != ContainerIds.INVENTORY && action.windowId != ContainerIds.OFFHAND) {
return false;
}
}
return true;
}
}
37 changes: 37 additions & 0 deletions src/main/java/org/itxtech/synapseapi/PendingPing.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.itxtech.synapseapi;

/** 单个未完成的服务端延迟请求;回包只用于关联,耗时取服务端单调时钟。 */
final class PendingPing {
private long startedNanos;
private boolean pending;

boolean begin(long nowNanos) {
if (this.pending) {
return false;
}
this.startedNanos = nowNanos;
this.pending = true;
return true;
}

long acknowledge(long token, long nowNanos) {
if (!this.pending || (token != this.startedNanos && token != this.startedNanos * 1000
&& token != this.startedNanos * 1000000)) {
return -1;
}
long elapsed = nowNanos - this.startedNanos;
if (elapsed < 0) {
return -1;
}
this.pending = false;
return elapsed;
}

boolean isPending() {
return this.pending;
}

void expire() {
this.pending = false;
}
}
18 changes: 15 additions & 3 deletions src/main/java/org/itxtech/synapseapi/SynapseAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public class SynapseAPI extends PluginBase implements Listener {
private Messenger messenger;
private JavaCustomPayloadMessenger javaCustomPayloadMessenger;
private boolean networkBroadcastPlayerMove;
private boolean mainThreadInput;
private int blobCacheChunkSendPreTick;

public static SynapseAPI getInstance() {
Expand All @@ -64,8 +65,13 @@ public boolean isAutoConnect() {
return autoConnect;
}

/** 新输入模式以有效提交发布替代 raw 广播;任一端关闭时保留旧路径。 */
public boolean isNetworkBroadcastPlayerMove() {
return networkBroadcastPlayerMove;
return networkBroadcastPlayerMove && (!mainThreadInput || getServer().getInputDispatcher() == null);
}

public boolean isMainThreadInput() {
return mainThreadInput;
}

public int getBlobCacheChunkSendPreTick() {
Expand All @@ -85,6 +91,7 @@ public void onEnable() {

this.messenger = new StandardMessenger();
this.javaCustomPayloadMessenger = new StandardJavaCustomPayloadMessenger();
this.mainThreadInput = this.getConfig().getBoolean("main-thread-input", false);
loadEntries();

this.getServer().getPluginManager().registerEvents(this, this);
Expand Down Expand Up @@ -381,8 +388,13 @@ public JavaCustomPayloadMessenger getJavaCustomPayloadMessenger() {

@EventHandler
public void onPlayerQuit(final PlayerQuitEvent event) {
if (this.javaCustomPayloadMessenger != null && event.getPlayer() instanceof SynapsePlayer player) {
this.javaCustomPayloadMessenger.unregisterPlayerChannels(player);
if (event.getPlayer() instanceof SynapsePlayer player) {
if (player.isMainThreadInputEnabled()) {
player.getSynapseEntry().handlePlayerQuit(player);
}
if (this.javaCustomPayloadMessenger != null) {
this.javaCustomPayloadMessenger.unregisterPlayerChannels(player);
}
}
}

Expand Down
Loading
Loading