mirror of
https://github.com/Minestom/Minestom.git
synced 2024-12-27 11:38:03 +01:00
Merge branch 'master' into master
This commit is contained in:
commit
2c18312988
10
.github/workflows/tests.yml
vendored
10
.github/workflows/tests.yml
vendored
@ -19,6 +19,16 @@ jobs:
|
||||
java-version: 1.11
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
- name: Setup gradle cache
|
||||
uses: burrunan/gradle-cache-action@v1
|
||||
with:
|
||||
save-generated-gradle-jars: false
|
||||
save-local-build-cache: false
|
||||
save-gradle-dependencies-cache: true
|
||||
save-maven-dependencies-cache: true
|
||||
# Ignore some of the paths when caching Maven Local repository
|
||||
maven-local-ignore-paths: |
|
||||
net/minestom/
|
||||
- name: Build Minestom
|
||||
run: ./gradlew build
|
||||
- name: Run Minestom tests
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -52,4 +52,4 @@ gradle-app.setting
|
||||
/src/main/java/com/mcecraft/
|
||||
|
||||
# When running the demo we generate the extensions folder
|
||||
extensions/
|
||||
/extensions/
|
||||
|
@ -129,6 +129,8 @@ dependencies {
|
||||
// SLF4J is the base logger for most libraries, therefore we can hook it into log4j2.
|
||||
api 'org.apache.logging.log4j:log4j-slf4j-impl:2.14.0'
|
||||
|
||||
// Guava 21.0+ required for Mixin, but Authlib imports 17.0
|
||||
api 'com.google.guava:guava:21.0'
|
||||
api 'com.mojang:authlib:1.5.21'
|
||||
|
||||
// Code modification
|
||||
@ -143,6 +145,8 @@ dependencies {
|
||||
api 'com.github.MadMartian:hydrazine-path-finding:1.4.2'
|
||||
|
||||
api "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
|
||||
|
||||
// NBT parsing/manipulation/saving
|
||||
api("com.github.jglrxavpok:Hephaistos:${project.hephaistos_version}")
|
||||
api("com.github.jglrxavpok:Hephaistos:${project.hephaistos_version}:gson")
|
||||
api("com.github.jglrxavpok:Hephaistos:${project.hephaistos_version}") {
|
||||
@ -151,6 +155,8 @@ dependencies {
|
||||
}
|
||||
}
|
||||
|
||||
implementation "com.github.Minestom:DependencyGetter:v1.0.1"
|
||||
|
||||
// LWJGL, for map rendering
|
||||
lwjglApi platform("org.lwjgl:lwjgl-bom:$lwjglVersion")
|
||||
|
||||
|
1
gradle/wrapper/gradle-wrapper.properties
vendored
1
gradle/wrapper/gradle-wrapper.properties
vendored
@ -4,3 +4,4 @@ distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip
|
||||
distributionSha256Sum=0f316a67b971b7b571dac7215dcf2591a30994b3450e0629925ffcfe2c68cc5c
|
||||
|
@ -1,6 +1,6 @@
|
||||
package net.minestom.server;
|
||||
|
||||
import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader;
|
||||
import net.minestom.server.extras.selfmodification.MinestomRootClassLoader;
|
||||
import net.minestom.server.extras.selfmodification.mixins.MixinCodeModifier;
|
||||
import net.minestom.server.extras.selfmodification.mixins.MixinServiceMinestom;
|
||||
import org.spongepowered.asm.launch.MixinBootstrap;
|
||||
@ -12,15 +12,15 @@ import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Used to launch Minestom with the {@link MinestomOverwriteClassLoader} to allow for self-modifications
|
||||
* Used to launch Minestom with the {@link MinestomRootClassLoader} to allow for self-modifications
|
||||
*/
|
||||
public final class Bootstrap {
|
||||
|
||||
public static void bootstrap(String mainClassFullName, String[] args) {
|
||||
try {
|
||||
ClassLoader classLoader = MinestomOverwriteClassLoader.getInstance();
|
||||
ClassLoader classLoader = MinestomRootClassLoader.getInstance();
|
||||
startMixin(args);
|
||||
MinestomOverwriteClassLoader.getInstance().addCodeModifier(new MixinCodeModifier());
|
||||
MinestomRootClassLoader.getInstance().addCodeModifier(new MixinCodeModifier());
|
||||
|
||||
MixinServiceMinestom.gotoPreinitPhase();
|
||||
// ensure extensions are loaded when starting the server
|
||||
@ -53,6 +53,6 @@ public final class Bootstrap {
|
||||
doInit.invoke(null, CommandLineOptions.ofArgs(Arrays.asList(args)));
|
||||
|
||||
MixinBootstrap.getPlatform().inject();
|
||||
Mixins.getConfigs().forEach(c -> MinestomOverwriteClassLoader.getInstance().protectedPackages.add(c.getConfig().getMixinPackage()));
|
||||
Mixins.getConfigs().forEach(c -> MinestomRootClassLoader.getInstance().protectedPackages.add(c.getConfig().getMixinPackage()));
|
||||
}
|
||||
}
|
||||
|
@ -77,7 +77,7 @@ public final class MinecraftServer {
|
||||
public static final String THREAD_NAME_TICK = "Ms-Tick";
|
||||
|
||||
public static final String THREAD_NAME_BLOCK_BATCH = "Ms-BlockBatchPool";
|
||||
public static final int THREAD_COUNT_BLOCK_BATCH = 2;
|
||||
public static final int THREAD_COUNT_BLOCK_BATCH = 4;
|
||||
|
||||
public static final String THREAD_NAME_SCHEDULER = "Ms-SchedulerPool";
|
||||
public static final int THREAD_COUNT_SCHEDULER = 1;
|
||||
@ -124,7 +124,7 @@ public final class MinecraftServer {
|
||||
private static boolean initialized;
|
||||
private static boolean started;
|
||||
|
||||
private static int chunkViewDistance = 10;
|
||||
private static int chunkViewDistance = 8;
|
||||
private static int entityViewDistance = 5;
|
||||
private static int compressionThreshold = 256;
|
||||
private static ResponseDataConsumer responseDataConsumer;
|
||||
|
@ -3,7 +3,7 @@ package net.minestom.server;
|
||||
import net.minestom.server.entity.EntityManager;
|
||||
import net.minestom.server.instance.Instance;
|
||||
import net.minestom.server.instance.InstanceManager;
|
||||
import net.minestom.server.thread.PerGroupChunkProvider;
|
||||
import net.minestom.server.thread.PerInstanceThreadProvider;
|
||||
import net.minestom.server.thread.ThreadProvider;
|
||||
import net.minestom.server.utils.thread.MinestomThread;
|
||||
import net.minestom.server.utils.validate.Check;
|
||||
@ -33,7 +33,8 @@ public final class UpdateManager {
|
||||
|
||||
{
|
||||
// DEFAULT THREAD PROVIDER
|
||||
threadProvider = new PerGroupChunkProvider();
|
||||
//threadProvider = new PerGroupChunkProvider();
|
||||
threadProvider = new PerInstanceThreadProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -104,6 +104,16 @@ public final class CommandManager {
|
||||
this.dispatcher.register(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a command from the currently registered commands.
|
||||
* Does nothing if the command was not registered before
|
||||
*
|
||||
* @param command the command to remove
|
||||
*/
|
||||
public void unregister(@NotNull Command command) {
|
||||
this.dispatcher.unregister(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link Command} registered by {@link #register(Command)}.
|
||||
*
|
||||
|
@ -37,6 +37,14 @@ public class CommandDispatcher {
|
||||
this.commands.add(command);
|
||||
}
|
||||
|
||||
public void unregister(Command command) {
|
||||
commandMap.remove(command.getName().toLowerCase());
|
||||
for(String alias : command.getAliases()) {
|
||||
this.commandMap.remove(alias.toLowerCase());
|
||||
}
|
||||
commands.remove(command);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given command.
|
||||
*
|
||||
|
@ -31,7 +31,9 @@ import net.minestom.server.utils.callback.OptionalCallback;
|
||||
import net.minestom.server.utils.chunk.ChunkUtils;
|
||||
import net.minestom.server.utils.entity.EntityUtils;
|
||||
import net.minestom.server.utils.player.PlayerUtils;
|
||||
import net.minestom.server.utils.time.CooldownUtils;
|
||||
import net.minestom.server.utils.time.TimeUnit;
|
||||
import net.minestom.server.utils.time.UpdateOption;
|
||||
import net.minestom.server.utils.validate.Check;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@ -103,9 +105,9 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
|
||||
private long lastUpdate;
|
||||
private final EntityType entityType;
|
||||
|
||||
// Network synchronization
|
||||
private static final long SYNCHRONIZATION_DELAY = 1500; // In ms
|
||||
private long lastSynchronizationTime;
|
||||
// Network synchronization, send the absolute position of the entity each X milliseconds
|
||||
private static final UpdateOption SYNCHRONIZATION_COOLDOWN = new UpdateOption(1500, TimeUnit.MILLISECOND);
|
||||
private long lastAbsoluteSynchronizationTime;
|
||||
|
||||
// Events
|
||||
private final Map<Class<? extends Event>, Collection<EventCallback>> eventCallbacks = new ConcurrentHashMap<>();
|
||||
@ -543,8 +545,8 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
|
||||
}
|
||||
|
||||
// Scheduled synchronization
|
||||
if (time - lastSynchronizationTime >= SYNCHRONIZATION_DELAY) {
|
||||
lastSynchronizationTime = time;
|
||||
if (!CooldownUtils.hasCooldown(time, lastAbsoluteSynchronizationTime, SYNCHRONIZATION_COOLDOWN)) {
|
||||
this.lastAbsoluteSynchronizationTime = time;
|
||||
sendSynchronization();
|
||||
}
|
||||
|
||||
@ -672,7 +674,7 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
|
||||
*/
|
||||
@Nullable
|
||||
public Chunk getChunk() {
|
||||
return instance.getChunkAt(lastX, lastZ);
|
||||
return instance.getChunkAt(position.getX(), position.getZ());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1354,7 +1356,7 @@ public abstract class Entity implements Viewable, EventHandler, DataContainer, P
|
||||
* Asks for a synchronization (position) to happen during next entity tick.
|
||||
*/
|
||||
public void askSynchronization() {
|
||||
this.lastSynchronizationTime = 0;
|
||||
this.lastAbsoluteSynchronizationTime = 0;
|
||||
}
|
||||
|
||||
private boolean shouldUpdate(long time) {
|
||||
|
@ -56,7 +56,6 @@ import net.minestom.server.utils.callback.OptionalCallback;
|
||||
import net.minestom.server.utils.chunk.ChunkCallback;
|
||||
import net.minestom.server.utils.chunk.ChunkUtils;
|
||||
import net.minestom.server.utils.instance.InstanceUtils;
|
||||
import net.minestom.server.utils.player.PlayerUtils;
|
||||
import net.minestom.server.utils.time.CooldownUtils;
|
||||
import net.minestom.server.utils.time.TimeUnit;
|
||||
import net.minestom.server.utils.time.UpdateOption;
|
||||
@ -73,12 +72,50 @@ import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Those are the major actors of the server,
|
||||
* they are not necessary backed by a {@link NettyPlayerConnection} as shown by {@link FakePlayer}
|
||||
* they are not necessary backed by a {@link NettyPlayerConnection} as shown by {@link FakePlayer}.
|
||||
* <p>
|
||||
* You can easily create your own implementation of this and use it with {@link ConnectionManager#setPlayerProvider(PlayerProvider)}.
|
||||
*/
|
||||
public class Player extends LivingEntity implements CommandSender {
|
||||
|
||||
/**
|
||||
* @see #getPlayerSynchronizationGroup()
|
||||
*/
|
||||
private static volatile int playerSynchronizationGroup = 50;
|
||||
|
||||
/**
|
||||
* For the number of viewers that a player has, the position synchronization packet will be sent
|
||||
* every 1 tick + (viewers/{@code playerSynchronizationGroup}).
|
||||
* (eg with a value of 100, having 300 viewers means sending the synchronization packet every 3 ticks)
|
||||
* <p>
|
||||
* Used to prevent sending exponentially more packets and therefore reduce network load.
|
||||
*
|
||||
* @return the viewers count which would result in a 1 tick delay
|
||||
*/
|
||||
public static int getPlayerSynchronizationGroup() {
|
||||
return playerSynchronizationGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the viewers count resulting in an additional delay of 1 tick for the position synchronization.
|
||||
*
|
||||
* @param playerSynchronizationGroup the new synchronization group size
|
||||
* @see #getPlayerSynchronizationGroup()
|
||||
*/
|
||||
public static void setPlayerSynchronizationGroup(int playerSynchronizationGroup) {
|
||||
Player.playerSynchronizationGroup = playerSynchronizationGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of tick between each position synchronization.
|
||||
*
|
||||
* @param viewersCount the player viewers count
|
||||
* @return the number of tick between each position synchronization.
|
||||
*/
|
||||
public static int getPlayerSynchronizationTickDelay(int viewersCount) {
|
||||
return viewersCount / playerSynchronizationGroup + 1;
|
||||
}
|
||||
|
||||
private long lastKeepAlive;
|
||||
private boolean answerKeepAlive;
|
||||
|
||||
@ -131,6 +168,10 @@ public class Player extends LivingEntity implements CommandSender {
|
||||
private byte targetStage; // The current stage of the target block, only if multi player breaking is disabled
|
||||
private final Set<Player> targetBreakers = new HashSet<>(1); // Only used if multi player breaking is disabled, contains only this player
|
||||
|
||||
// Position synchronization with viewers
|
||||
private long lastPlayerSynchronizationTime;
|
||||
private float lastPlayerSyncX, lastPlayerSyncY, lastPlayerSyncZ, lastPlayerSyncYaw, lastPlayerSyncPitch;
|
||||
|
||||
// Experience orb pickup
|
||||
protected UpdateOption experiencePickupCooldown = new UpdateOption(10, TimeUnit.TICK);
|
||||
private long lastExperiencePickupCheckTime;
|
||||
@ -299,13 +340,8 @@ public class Player extends LivingEntity implements CommandSender {
|
||||
@Override
|
||||
public void update(long time) {
|
||||
|
||||
// Flush all pending packets
|
||||
if (PlayerUtils.isNettyClient(this)) {
|
||||
((NettyPlayerConnection) playerConnection).getChannel().flush();
|
||||
}
|
||||
|
||||
// Network tick verification
|
||||
playerConnection.updateStats();
|
||||
// Network tick
|
||||
this.playerConnection.update();
|
||||
|
||||
// Process received packets
|
||||
ClientPlayPacket packet;
|
||||
@ -412,9 +448,16 @@ public class Player extends LivingEntity implements CommandSender {
|
||||
callEvent(PlayerTickEvent.class, playerTickEvent);
|
||||
|
||||
// Multiplayer sync
|
||||
final boolean positionChanged = position.getX() != lastX || position.getY() != lastY || position.getZ() != lastZ;
|
||||
final boolean viewChanged = position.getYaw() != lastYaw || position.getPitch() != lastPitch;
|
||||
if (!viewers.isEmpty()) {
|
||||
final boolean positionChanged = position.getX() != lastPlayerSyncX ||
|
||||
position.getY() != lastPlayerSyncY ||
|
||||
position.getZ() != lastPlayerSyncZ;
|
||||
final boolean viewChanged = position.getYaw() != lastPlayerSyncYaw ||
|
||||
position.getPitch() != lastPlayerSyncPitch;
|
||||
if (!viewers.isEmpty() &&
|
||||
!CooldownUtils.hasCooldown(time, lastPlayerSynchronizationTime,
|
||||
TimeUnit.TICK, getPlayerSynchronizationTickDelay(viewers.size()))) {
|
||||
this.lastPlayerSynchronizationTime = time;
|
||||
|
||||
if (positionChanged || viewChanged) {
|
||||
// Player moved since last time
|
||||
|
||||
@ -423,9 +466,9 @@ public class Player extends LivingEntity implements CommandSender {
|
||||
if (positionChanged && viewChanged) {
|
||||
EntityPositionAndRotationPacket entityPositionAndRotationPacket = new EntityPositionAndRotationPacket();
|
||||
entityPositionAndRotationPacket.entityId = getEntityId();
|
||||
entityPositionAndRotationPacket.deltaX = (short) ((position.getX() * 32 - lastX * 32) * 128);
|
||||
entityPositionAndRotationPacket.deltaY = (short) ((position.getY() * 32 - lastY * 32) * 128);
|
||||
entityPositionAndRotationPacket.deltaZ = (short) ((position.getZ() * 32 - lastZ * 32) * 128);
|
||||
entityPositionAndRotationPacket.deltaX = (short) ((position.getX() * 32 - lastPlayerSyncX * 32) * 128);
|
||||
entityPositionAndRotationPacket.deltaY = (short) ((position.getY() * 32 - lastPlayerSyncY * 32) * 128);
|
||||
entityPositionAndRotationPacket.deltaZ = (short) ((position.getZ() * 32 - lastPlayerSyncZ * 32) * 128);
|
||||
entityPositionAndRotationPacket.yaw = position.getYaw();
|
||||
entityPositionAndRotationPacket.pitch = position.getPitch();
|
||||
entityPositionAndRotationPacket.onGround = onGround;
|
||||
@ -434,9 +477,9 @@ public class Player extends LivingEntity implements CommandSender {
|
||||
} else if (positionChanged) {
|
||||
EntityPositionPacket entityPositionPacket = new EntityPositionPacket();
|
||||
entityPositionPacket.entityId = getEntityId();
|
||||
entityPositionPacket.deltaX = (short) ((position.getX() * 32 - lastX * 32) * 128);
|
||||
entityPositionPacket.deltaY = (short) ((position.getY() * 32 - lastY * 32) * 128);
|
||||
entityPositionPacket.deltaZ = (short) ((position.getZ() * 32 - lastZ * 32) * 128);
|
||||
entityPositionPacket.deltaX = (short) ((position.getX() * 32 - lastPlayerSyncX * 32) * 128);
|
||||
entityPositionPacket.deltaY = (short) ((position.getY() * 32 - lastPlayerSyncY * 32) * 128);
|
||||
entityPositionPacket.deltaZ = (short) ((position.getZ() * 32 - lastPlayerSyncZ * 32) * 128);
|
||||
entityPositionPacket.onGround = onGround;
|
||||
|
||||
updatePacket = entityPositionPacket;
|
||||
@ -472,16 +515,17 @@ public class Player extends LivingEntity implements CommandSender {
|
||||
entityMovementPacket.entityId = getEntityId();
|
||||
sendPacketToViewers(entityMovementPacket);
|
||||
}
|
||||
}
|
||||
|
||||
if (positionChanged) {
|
||||
lastX = position.getX();
|
||||
lastY = position.getY();
|
||||
lastZ = position.getZ();
|
||||
}
|
||||
if (viewChanged) {
|
||||
lastYaw = position.getYaw();
|
||||
lastPitch = position.getPitch();
|
||||
// Update sync data
|
||||
if (positionChanged) {
|
||||
lastPlayerSyncX = position.getX();
|
||||
lastPlayerSyncY = position.getY();
|
||||
lastPlayerSyncZ = position.getZ();
|
||||
}
|
||||
if (viewChanged) {
|
||||
lastPlayerSyncYaw = position.getYaw();
|
||||
lastPlayerSyncPitch = position.getPitch();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -659,7 +703,7 @@ public class Player extends LivingEntity implements CommandSender {
|
||||
sendDimension(instanceDimensionType);
|
||||
}
|
||||
|
||||
final long[] visibleChunks = ChunkUtils.getChunksInRange(position, getChunkRange());
|
||||
final long[] visibleChunks = ChunkUtils.getChunksInRange(firstSpawn ? getRespawnPoint() : position, getChunkRange());
|
||||
final int length = visibleChunks.length;
|
||||
|
||||
AtomicInteger counter = new AtomicInteger(0);
|
||||
@ -703,11 +747,11 @@ public class Player extends LivingEntity implements CommandSender {
|
||||
*/
|
||||
private void spawnPlayer(Instance instance, boolean firstSpawn) {
|
||||
this.viewableEntities.forEach(entity -> entity.removeViewer(this));
|
||||
super.setInstance(instance);
|
||||
|
||||
if (firstSpawn) {
|
||||
teleport(getRespawnPoint());
|
||||
this.position = getRespawnPoint();
|
||||
}
|
||||
super.setInstance(instance);
|
||||
|
||||
PlayerSpawnEvent spawnEvent = new PlayerSpawnEvent(this, instance, firstSpawn);
|
||||
callEvent(PlayerSpawnEvent.class, spawnEvent);
|
||||
|
@ -8,6 +8,10 @@ import org.jetbrains.annotations.NotNull;
|
||||
/**
|
||||
* Called when a {@link Player} start digging a block,
|
||||
* can be used to forbid the {@link Player} from mining it.
|
||||
* <p>
|
||||
* Be aware that cancelling this event does not necessary prevent the player from breaking the block
|
||||
* (could be because of high latency or a modified client) so cancelling {@link PlayerBlockBreakEvent} is also necessary.
|
||||
* Could be fixed in future Minestom version.
|
||||
*/
|
||||
public class PlayerStartDiggingEvent extends CancellableEvent {
|
||||
|
||||
|
@ -0,0 +1,171 @@
|
||||
package net.minestom.server.extensions;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
final class DiscoveredExtension {
|
||||
|
||||
public final static Logger LOGGER = LoggerFactory.getLogger(DiscoveredExtension.class);
|
||||
|
||||
public static final String NAME_REGEX = "[A-Za-z][_A-Za-z0-9]+";
|
||||
private String name;
|
||||
private String entrypoint;
|
||||
private String version;
|
||||
private String mixinConfig;
|
||||
private String[] authors;
|
||||
private String[] codeModifiers;
|
||||
private String[] dependencies;
|
||||
private ExternalDependencies externalDependencies;
|
||||
transient List<URL> files = new LinkedList<>();
|
||||
transient LoadStatus loadStatus = LoadStatus.LOAD_SUCCESS;
|
||||
transient private File originalJar;
|
||||
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getEntrypoint() {
|
||||
return entrypoint;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getMixinConfig() {
|
||||
return mixinConfig;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String[] getAuthors() {
|
||||
return authors;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String[] getCodeModifiers() {
|
||||
if (codeModifiers == null) {
|
||||
codeModifiers = new String[0];
|
||||
}
|
||||
return codeModifiers;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String[] getDependencies() {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public ExternalDependencies getExternalDependencies() {
|
||||
return externalDependencies;
|
||||
}
|
||||
|
||||
void setOriginalJar(@Nullable File file) {
|
||||
originalJar = file;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
File getOriginalJar() {
|
||||
return originalJar;
|
||||
}
|
||||
|
||||
static void verifyIntegrity(@NotNull DiscoveredExtension extension) {
|
||||
if (extension.name == null) {
|
||||
StringBuilder fileList = new StringBuilder();
|
||||
for (URL f : extension.files) {
|
||||
fileList.append(f.toExternalForm()).append(", ");
|
||||
}
|
||||
LOGGER.error("Extension with no name. (at {}})", fileList);
|
||||
LOGGER.error("Extension at ({}) will not be loaded.", fileList);
|
||||
extension.loadStatus = DiscoveredExtension.LoadStatus.INVALID_NAME;
|
||||
|
||||
// To ensure @NotNull: name = INVALID_NAME
|
||||
extension.name = extension.loadStatus.name();
|
||||
return;
|
||||
}
|
||||
if (!extension.name.matches(NAME_REGEX)) {
|
||||
LOGGER.error("Extension '{}' specified an invalid name.", extension.name);
|
||||
LOGGER.error("Extension '{}' will not be loaded.", extension.name);
|
||||
extension.loadStatus = DiscoveredExtension.LoadStatus.INVALID_NAME;
|
||||
|
||||
// To ensure @NotNull: name = INVALID_NAME
|
||||
extension.name = extension.loadStatus.name();
|
||||
return;
|
||||
}
|
||||
if (extension.entrypoint == null) {
|
||||
LOGGER.error("Extension '{}' did not specify an entry point (via 'entrypoint').", extension.name);
|
||||
LOGGER.error("Extension '{}' will not be loaded.", extension.name);
|
||||
extension.loadStatus = DiscoveredExtension.LoadStatus.NO_ENTRYPOINT;
|
||||
|
||||
// To ensure @NotNull: entrypoint = NO_ENTRYPOINT
|
||||
extension.entrypoint = extension.loadStatus.name();
|
||||
return;
|
||||
}
|
||||
// Handle defaults
|
||||
// If we reach this code, then the extension will most likely be loaded:
|
||||
if (extension.version == null) {
|
||||
LOGGER.warn("Extension '{}' did not specify a version.", extension.name);
|
||||
LOGGER.warn("Extension '{}' will continue to load but should specify a plugin version.", extension.name);
|
||||
extension.version = "Unspecified";
|
||||
}
|
||||
if (extension.mixinConfig == null) {
|
||||
extension.mixinConfig = "";
|
||||
}
|
||||
if (extension.authors == null) {
|
||||
extension.authors = new String[0];
|
||||
}
|
||||
if (extension.codeModifiers == null) {
|
||||
extension.codeModifiers = new String[0];
|
||||
}
|
||||
// No dependencies were specified
|
||||
if (extension.dependencies == null) {
|
||||
extension.dependencies = new String[0];
|
||||
}
|
||||
// No external dependencies were specified;
|
||||
if (extension.externalDependencies == null) {
|
||||
extension.externalDependencies = new ExternalDependencies();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum LoadStatus {
|
||||
LOAD_SUCCESS("Actually, it did not fail. This message should not have been printed."),
|
||||
MISSING_DEPENDENCIES("Missing dependencies, check your logs."),
|
||||
INVALID_NAME("Invalid name."),
|
||||
NO_ENTRYPOINT("No entrypoint specified."),
|
||||
FAILED_TO_SETUP_CLASSLOADER("Extension classloader could not be setup."),
|
||||
LOAD_FAILED("Load failed. See logs for more information."),
|
||||
;
|
||||
|
||||
private final String message;
|
||||
|
||||
LoadStatus(@NotNull String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
static final class ExternalDependencies {
|
||||
Repository[] repositories = new Repository[0];
|
||||
String[] artifacts = new String[0];
|
||||
|
||||
static class Repository {
|
||||
String name = "";
|
||||
String url = "";
|
||||
}
|
||||
}
|
||||
}
|
@ -3,11 +3,15 @@ package net.minestom.server.extensions;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class Extension {
|
||||
|
||||
// Set by reflection
|
||||
@SuppressWarnings("unused")
|
||||
private ExtensionDescription description;
|
||||
// Set by reflection
|
||||
@SuppressWarnings("unused")
|
||||
private Logger logger;
|
||||
|
||||
protected Extension() {
|
||||
@ -34,24 +38,36 @@ public abstract class Extension {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after postTerminate when reloading an extension
|
||||
*/
|
||||
public void unload() {
|
||||
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public ExtensionDescription getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected Logger getLogger() {
|
||||
return logger;
|
||||
}
|
||||
|
||||
protected static class ExtensionDescription {
|
||||
|
||||
public static class ExtensionDescription {
|
||||
private final String name;
|
||||
private final String version;
|
||||
private final List<String> authors;
|
||||
private final List<String> dependents = new ArrayList<>();
|
||||
private final DiscoveredExtension origin;
|
||||
|
||||
protected ExtensionDescription(@NotNull String name, @NotNull String version, @NotNull List<String> authors) {
|
||||
ExtensionDescription(@NotNull String name, @NotNull String version, @NotNull List<String> authors, @NotNull DiscoveredExtension origin) {
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
this.authors = authors;
|
||||
this.origin = origin;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ -68,5 +84,15 @@ public abstract class Extension {
|
||||
public List<String> getAuthors() {
|
||||
return authors;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<String> getDependents() {
|
||||
return dependents;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
DiscoveredExtension getOrigin() {
|
||||
return origin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,54 @@
|
||||
package net.minestom.server.extensions;
|
||||
|
||||
import net.minestom.dependencies.DependencyResolver;
|
||||
import net.minestom.dependencies.ResolvedDependency;
|
||||
import net.minestom.dependencies.UnresolvedDependencyException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Does NOT relocate extensions
|
||||
*/
|
||||
public class ExtensionDependencyResolver implements DependencyResolver {
|
||||
|
||||
private Map<String, DiscoveredExtension> extensionMap = new HashMap<>();
|
||||
|
||||
public ExtensionDependencyResolver(List<DiscoveredExtension> extensions) {
|
||||
for(DiscoveredExtension ext : extensions) {
|
||||
extensionMap.put(ext.getName(), ext);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ResolvedDependency resolve(@NotNull String extensionName, @NotNull File file) throws UnresolvedDependencyException {
|
||||
if(extensionMap.containsKey(extensionName)) {
|
||||
DiscoveredExtension ext = extensionMap.get(extensionName);
|
||||
// convert extension URLs to subdependencies
|
||||
// FIXME: this is not a deep conversion, this might create an issue in this scenario with different classloaders:
|
||||
// A depends on an external lib (Ext<-A)
|
||||
// B depends on A (A<-B)
|
||||
// When loading B, with no deep conversion, Ext will not be added to the list of dependencies (because it is not a direct dependency)
|
||||
// But when trying to call/access code from extension A, the parts dependent on Ext won't be inside B's dependencies, triggering a ClassNotFoundException
|
||||
List<ResolvedDependency> deps = new LinkedList<>();
|
||||
for(URL u : ext.files) {
|
||||
deps.add(new ResolvedDependency(u.toExternalForm(), u.toExternalForm(), "", u, new LinkedList<>()));
|
||||
}
|
||||
return new ResolvedDependency(ext.getName(), ext.getName(), ext.getVersion(), ext.files.get(0), deps);
|
||||
}
|
||||
throw new UnresolvedDependencyException("No extension named "+extensionName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String list = extensionMap.values().stream().map(entry -> entry.getName()).collect(Collectors.joining(", "));
|
||||
return "ExtensionDependencyResolver[" + list + "]";
|
||||
}
|
||||
}
|
@ -1,7 +1,10 @@
|
||||
package net.minestom.server.extensions;
|
||||
|
||||
import com.google.gson.*;
|
||||
import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader;
|
||||
import com.google.gson.Gson;
|
||||
import net.minestom.dependencies.DependencyGetter;
|
||||
import net.minestom.dependencies.maven.MavenRepository;
|
||||
import net.minestom.server.extras.selfmodification.MinestomExtensionClassLoader;
|
||||
import net.minestom.server.extras.selfmodification.MinestomRootClassLoader;
|
||||
import net.minestom.server.utils.validate.Check;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@ -13,13 +16,13 @@ import java.io.*;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipFile;
|
||||
|
||||
public final class ExtensionManager {
|
||||
public class ExtensionManager {
|
||||
|
||||
public final static Logger LOGGER = LoggerFactory.getLogger(ExtensionManager.class);
|
||||
|
||||
@ -27,11 +30,15 @@ public final class ExtensionManager {
|
||||
private final static String INDEV_RESOURCES_FOLDER = "minestom.extension.indevfolder.resources";
|
||||
private final static Gson GSON = new Gson();
|
||||
|
||||
private final Map<String, URLClassLoader> extensionLoaders = new HashMap<>();
|
||||
private final Map<String, MinestomExtensionClassLoader> extensionLoaders = new HashMap<>();
|
||||
private final Map<String, Extension> extensions = new HashMap<>();
|
||||
private final File extensionFolder = new File("extensions");
|
||||
private final File dependenciesFolder = new File(extensionFolder, ".libs");
|
||||
private boolean loaded;
|
||||
|
||||
private final List<Extension> extensionList = new ArrayList<>();
|
||||
private final List<Extension> immutableExtensionListView = Collections.unmodifiableList(extensionList);
|
||||
|
||||
public ExtensionManager() {
|
||||
}
|
||||
|
||||
@ -46,146 +53,156 @@ public final class ExtensionManager {
|
||||
}
|
||||
}
|
||||
|
||||
final List<DiscoveredExtension> discoveredExtensions = discoverExtensions();
|
||||
if (!dependenciesFolder.exists()) {
|
||||
if (!dependenciesFolder.mkdirs()) {
|
||||
LOGGER.error("Could not find nor create the extension dependencies folder, extensions will not be loaded!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
List<DiscoveredExtension> discoveredExtensions = discoverExtensions();
|
||||
discoveredExtensions = generateLoadOrder(discoveredExtensions);
|
||||
loadDependencies(discoveredExtensions);
|
||||
// remove invalid extensions
|
||||
discoveredExtensions.removeIf(ext -> ext.loadStatus != DiscoveredExtension.LoadStatus.LOAD_SUCCESS);
|
||||
|
||||
for (DiscoveredExtension discoveredExtension : discoveredExtensions) {
|
||||
try {
|
||||
setupClassLoader(discoveredExtension);
|
||||
} catch (Exception e) {
|
||||
discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.FAILED_TO_SETUP_CLASSLOADER;
|
||||
e.printStackTrace();
|
||||
LOGGER.error("Failed to load extension {}", discoveredExtension.getName());
|
||||
LOGGER.error("Failed to load extension", e);
|
||||
}
|
||||
}
|
||||
|
||||
// remove invalid extensions
|
||||
discoveredExtensions.removeIf(ext -> ext.loadStatus != DiscoveredExtension.LoadStatus.LOAD_SUCCESS);
|
||||
setupCodeModifiers(discoveredExtensions);
|
||||
|
||||
for (DiscoveredExtension discoveredExtension : discoveredExtensions) {
|
||||
URLClassLoader loader;
|
||||
URL[] urls = new URL[discoveredExtension.files.length];
|
||||
try {
|
||||
for (int i = 0; i < urls.length; i++) {
|
||||
urls[i] = discoveredExtension.files[i].toURI().toURL();
|
||||
}
|
||||
loader = newClassLoader(urls);
|
||||
} catch (MalformedURLException e) {
|
||||
LOGGER.error("Failed to get URL.", e);
|
||||
continue;
|
||||
}
|
||||
// TODO: Can't we use discoveredExtension.description here? Someone should test that.
|
||||
final InputStream extensionInputStream = loader.getResourceAsStream("extension.json");
|
||||
if (extensionInputStream == null) {
|
||||
StringBuilder urlsString = new StringBuilder();
|
||||
for (int i = 0; i < urls.length; i++) {
|
||||
URL url = urls[i];
|
||||
if (i != 0) {
|
||||
urlsString.append(" ; ");
|
||||
}
|
||||
urlsString.append("'").append(url.toString()).append("'");
|
||||
}
|
||||
LOGGER.error("Failed to find extension.json in the urls '{}'.", urlsString);
|
||||
continue;
|
||||
}
|
||||
JsonObject extensionDescriptionJson = JsonParser.parseReader(new InputStreamReader(extensionInputStream)).getAsJsonObject();
|
||||
|
||||
final String mainClass = extensionDescriptionJson.get("entrypoint").getAsString();
|
||||
final String extensionName = extensionDescriptionJson.get("name").getAsString();
|
||||
// Check the validity of the extension's name.
|
||||
if (!extensionName.matches("[A-Za-z]+")) {
|
||||
LOGGER.error("Extension '{}' specified an invalid name.", extensionName);
|
||||
LOGGER.error("Extension '{}' will not be loaded.", extensionName);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get ExtensionDescription (authors, version etc.)
|
||||
Extension.ExtensionDescription extensionDescription;
|
||||
{
|
||||
String version;
|
||||
if (!extensionDescriptionJson.has("version")) {
|
||||
LOGGER.warn("Extension '{}' did not specify a version.", extensionName);
|
||||
LOGGER.warn("Extension '{}' will continue to load but should specify a plugin version.", extensionName);
|
||||
version = "Not Specified";
|
||||
} else {
|
||||
version = extensionDescriptionJson.get("version").getAsString();
|
||||
}
|
||||
List<String> authors;
|
||||
if (!extensionDescriptionJson.has("authors")) {
|
||||
authors = new ArrayList<>();
|
||||
} else {
|
||||
authors = Arrays.asList(new Gson().fromJson(extensionDescriptionJson.get("authors"), String[].class));
|
||||
}
|
||||
|
||||
extensionDescription = new Extension.ExtensionDescription(extensionName, version, authors);
|
||||
}
|
||||
|
||||
extensionLoaders.put(extensionName.toLowerCase(), loader);
|
||||
|
||||
if (extensions.containsKey(extensionName.toLowerCase())) {
|
||||
LOGGER.error("An extension called '{}' has already been registered.", extensionName);
|
||||
continue;
|
||||
}
|
||||
|
||||
Class<?> jarClass;
|
||||
try {
|
||||
jarClass = Class.forName(mainClass, true, loader);
|
||||
} catch (ClassNotFoundException e) {
|
||||
LOGGER.error("Could not find main class '{}' in extension '{}'.", mainClass, extensionName, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
Class<? extends Extension> extensionClass;
|
||||
try {
|
||||
extensionClass = jarClass.asSubclass(Extension.class);
|
||||
} catch (ClassCastException e) {
|
||||
LOGGER.error("Main class '{}' in '{}' does not extend the 'Extension' superclass.", mainClass, extensionName, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
Constructor<? extends Extension> constructor;
|
||||
try {
|
||||
constructor = extensionClass.getDeclaredConstructor();
|
||||
// Let's just make it accessible, plugin creators don't have to make this public.
|
||||
constructor.setAccessible(true);
|
||||
} catch (NoSuchMethodException e) {
|
||||
LOGGER.error("Main class '{}' in '{}' does not define a no-args constructor.", mainClass, extensionName, e);
|
||||
continue;
|
||||
}
|
||||
Extension extension = null;
|
||||
try {
|
||||
extension = constructor.newInstance();
|
||||
} catch (InstantiationException e) {
|
||||
LOGGER.error("Main class '{}' in '{}' cannot be an abstract class.", mainClass, extensionName, e);
|
||||
continue;
|
||||
} catch (IllegalAccessException ignored) {
|
||||
// We made it accessible, should not occur
|
||||
} catch (InvocationTargetException e) {
|
||||
LOGGER.error(
|
||||
"While instantiating the main class '{}' in '{}' an exception was thrown.",
|
||||
mainClass,
|
||||
extensionName,
|
||||
e.getTargetException()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set extension description
|
||||
try {
|
||||
Field descriptionField = extensionClass.getSuperclass().getDeclaredField("description");
|
||||
descriptionField.setAccessible(true);
|
||||
descriptionField.set(extension, extensionDescription);
|
||||
} catch (IllegalAccessException e) {
|
||||
// We made it accessible, should not occur
|
||||
} catch (NoSuchFieldException e) {
|
||||
LOGGER.error("Main class '{}' in '{}' has no description field.", mainClass, extensionName, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set logger
|
||||
try {
|
||||
Field descriptionField = extensionClass.getSuperclass().getDeclaredField("logger");
|
||||
descriptionField.setAccessible(true);
|
||||
descriptionField.set(extension, LoggerFactory.getLogger(extensionClass));
|
||||
} catch (IllegalAccessException e) {
|
||||
// We made it accessible, should not occur
|
||||
attemptSingleLoad(discoveredExtension);
|
||||
} catch (Exception e) {
|
||||
discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.LOAD_FAILED;
|
||||
e.printStackTrace();
|
||||
} catch (NoSuchFieldException e) {
|
||||
// This should also not occur (unless someone changed the logger in Extension superclass).
|
||||
LOGGER.error("Main class '{}' in '{}' has no logger field.", mainClass, extensionName, e);
|
||||
LOGGER.error("Failed to load extension {}", discoveredExtension.getName());
|
||||
LOGGER.error("Failed to load extension", e);
|
||||
}
|
||||
|
||||
extensions.put(extensionName.toLowerCase(), extension);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupClassLoader(DiscoveredExtension discoveredExtension) {
|
||||
String extensionName = discoveredExtension.getName();
|
||||
MinestomExtensionClassLoader loader;
|
||||
URL[] urls = discoveredExtension.files.toArray(new URL[0]);
|
||||
loader = newClassLoader(discoveredExtension, urls);
|
||||
extensionLoaders.put(extensionName.toLowerCase(), loader);
|
||||
}
|
||||
|
||||
private Extension attemptSingleLoad(DiscoveredExtension discoveredExtension) {
|
||||
// Create ExtensionDescription (authors, version etc.)
|
||||
String extensionName = discoveredExtension.getName();
|
||||
String mainClass = discoveredExtension.getEntrypoint();
|
||||
Extension.ExtensionDescription extensionDescription = new Extension.ExtensionDescription(
|
||||
extensionName,
|
||||
discoveredExtension.getVersion(),
|
||||
Arrays.asList(discoveredExtension.getAuthors()),
|
||||
discoveredExtension
|
||||
);
|
||||
|
||||
MinestomExtensionClassLoader loader = extensionLoaders.get(extensionName.toLowerCase());
|
||||
|
||||
if (extensions.containsKey(extensionName.toLowerCase())) {
|
||||
LOGGER.error("An extension called '{}' has already been registered.", extensionName);
|
||||
return null;
|
||||
}
|
||||
|
||||
Class<?> jarClass;
|
||||
try {
|
||||
jarClass = Class.forName(mainClass, true, loader);
|
||||
} catch (ClassNotFoundException e) {
|
||||
LOGGER.error("Could not find main class '{}' in extension '{}'.", mainClass, extensionName, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
Class<? extends Extension> extensionClass;
|
||||
try {
|
||||
extensionClass = jarClass.asSubclass(Extension.class);
|
||||
} catch (ClassCastException e) {
|
||||
LOGGER.error("Main class '{}' in '{}' does not extend the 'Extension' superclass.", mainClass, extensionName, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
Constructor<? extends Extension> constructor;
|
||||
try {
|
||||
constructor = extensionClass.getDeclaredConstructor();
|
||||
// Let's just make it accessible, plugin creators don't have to make this public.
|
||||
constructor.setAccessible(true);
|
||||
} catch (NoSuchMethodException e) {
|
||||
LOGGER.error("Main class '{}' in '{}' does not define a no-args constructor.", mainClass, extensionName, e);
|
||||
return null;
|
||||
}
|
||||
Extension extension = null;
|
||||
try {
|
||||
extension = constructor.newInstance();
|
||||
} catch (InstantiationException e) {
|
||||
LOGGER.error("Main class '{}' in '{}' cannot be an abstract class.", mainClass, extensionName, e);
|
||||
return null;
|
||||
} catch (IllegalAccessException ignored) {
|
||||
// We made it accessible, should not occur
|
||||
} catch (InvocationTargetException e) {
|
||||
LOGGER.error(
|
||||
"While instantiating the main class '{}' in '{}' an exception was thrown.",
|
||||
mainClass,
|
||||
extensionName,
|
||||
e.getTargetException()
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set extension description
|
||||
try {
|
||||
Field descriptionField = extensionClass.getSuperclass().getDeclaredField("description");
|
||||
descriptionField.setAccessible(true);
|
||||
descriptionField.set(extension, extensionDescription);
|
||||
} catch (IllegalAccessException e) {
|
||||
// We made it accessible, should not occur
|
||||
} catch (NoSuchFieldException e) {
|
||||
LOGGER.error("Main class '{}' in '{}' has no description field.", mainClass, extensionName, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set logger
|
||||
try {
|
||||
Field loggerField = extensionClass.getSuperclass().getDeclaredField("logger");
|
||||
loggerField.setAccessible(true);
|
||||
loggerField.set(extension, LoggerFactory.getLogger(extensionClass));
|
||||
} catch (IllegalAccessException e) {
|
||||
// We made it accessible, should not occur
|
||||
e.printStackTrace();
|
||||
} catch (NoSuchFieldException e) {
|
||||
// This should also not occur (unless someone changed the logger in Extension superclass).
|
||||
LOGGER.error("Main class '{}' in '{}' has no logger field.", mainClass, extensionName, e);
|
||||
}
|
||||
|
||||
// add dependents to pre-existing extensions, so that they can easily be found during reloading
|
||||
for (String dependency : discoveredExtension.getDependencies()) {
|
||||
Extension dep = extensions.get(dependency.toLowerCase());
|
||||
if (dep == null) {
|
||||
LOGGER.warn("Dependency {} of {} is null? This means the extension has been loaded without its dependency, which could cause issues later.", dependency, discoveredExtension.getName());
|
||||
} else {
|
||||
dep.getDescription().getDependents().add(discoveredExtension.getName());
|
||||
}
|
||||
}
|
||||
|
||||
extensionList.add(extension); // add to a list, as lists preserve order
|
||||
extensions.put(extensionName.toLowerCase(), extension);
|
||||
|
||||
return extension;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<DiscoveredExtension> discoverExtensions() {
|
||||
List<DiscoveredExtension> extensions = new LinkedList<>();
|
||||
@ -196,15 +213,9 @@ public final class ExtensionManager {
|
||||
if (!file.getName().endsWith(".jar")) {
|
||||
continue;
|
||||
}
|
||||
try (ZipFile f = new ZipFile(file);
|
||||
InputStreamReader reader = new InputStreamReader(f.getInputStream(f.getEntry("extension.json")))) {
|
||||
|
||||
DiscoveredExtension extension = new DiscoveredExtension();
|
||||
extension.files = new File[]{file};
|
||||
extension.description = GSON.fromJson(reader, JsonObject.class);
|
||||
DiscoveredExtension extension = discoverFromJar(file);
|
||||
if (extension != null && extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) {
|
||||
extensions.add(extension);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@ -214,10 +225,16 @@ public final class ExtensionManager {
|
||||
final String extensionClasses = System.getProperty(INDEV_CLASSES_FOLDER);
|
||||
final String extensionResources = System.getProperty(INDEV_RESOURCES_FOLDER);
|
||||
try (InputStreamReader reader = new InputStreamReader(new FileInputStream(new File(extensionResources, "extension.json")))) {
|
||||
DiscoveredExtension extension = new DiscoveredExtension();
|
||||
extension.files = new File[]{new File(extensionClasses), new File(extensionResources)};
|
||||
extension.description = GSON.fromJson(reader, JsonObject.class);
|
||||
extensions.add(extension);
|
||||
DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class);
|
||||
extension.files.add(new File(extensionClasses).toURI().toURL());
|
||||
extension.files.add(new File(extensionResources).toURI().toURL());
|
||||
|
||||
// Verify integrity and ensure defaults
|
||||
DiscoveredExtension.verifyIntegrity(extension);
|
||||
|
||||
if (extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) {
|
||||
extensions.add(extension);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@ -225,14 +242,186 @@ public final class ExtensionManager {
|
||||
return extensions;
|
||||
}
|
||||
|
||||
private DiscoveredExtension discoverFromJar(File file) {
|
||||
try (ZipFile f = new ZipFile(file);
|
||||
InputStreamReader reader = new InputStreamReader(f.getInputStream(f.getEntry("extension.json")))) {
|
||||
|
||||
DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class);
|
||||
extension.setOriginalJar(file);
|
||||
extension.files.add(file.toURI().toURL());
|
||||
|
||||
// Verify integrity and ensure defaults
|
||||
DiscoveredExtension.verifyIntegrity(extension);
|
||||
|
||||
return extension;
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<DiscoveredExtension> generateLoadOrder(List<DiscoveredExtension> discoveredExtensions) {
|
||||
// Do some mapping so we can map strings to extensions.
|
||||
Map<String, DiscoveredExtension> extensionMap = new HashMap<>();
|
||||
Map<DiscoveredExtension, List<DiscoveredExtension>> dependencyMap = new HashMap<>();
|
||||
for (DiscoveredExtension discoveredExtension : discoveredExtensions) {
|
||||
extensionMap.put(discoveredExtension.getName().toLowerCase(), discoveredExtension);
|
||||
}
|
||||
for (DiscoveredExtension discoveredExtension : discoveredExtensions) {
|
||||
|
||||
List<DiscoveredExtension> dependencies = Arrays.stream(discoveredExtension.getDependencies())
|
||||
.map(dependencyName -> {
|
||||
DiscoveredExtension dependencyExtension = extensionMap.get(dependencyName.toLowerCase());
|
||||
// Specifies an extension we don't have.
|
||||
if (dependencyExtension == null) {
|
||||
// attempt to see if it is not already loaded (happens with dynamic (re)loading)
|
||||
if (extensions.containsKey(dependencyName.toLowerCase())) {
|
||||
return extensions.get(dependencyName.toLowerCase()).getDescription().getOrigin();
|
||||
} else {
|
||||
LOGGER.error("Extension {} requires an extension called {}.", discoveredExtension.getName(), dependencyName);
|
||||
LOGGER.error("However the extension {} could not be found.", dependencyName);
|
||||
LOGGER.error("Therefore {} will not be loaded.", discoveredExtension.getName());
|
||||
discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.MISSING_DEPENDENCIES;
|
||||
}
|
||||
}
|
||||
// This will return null for an unknown-extension
|
||||
return extensionMap.get(dependencyName.toLowerCase());
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
// If the list contains null ignore it.
|
||||
if (!dependencies.contains(null)) {
|
||||
dependencyMap.put(
|
||||
discoveredExtension,
|
||||
dependencies
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// List containing the real load order.
|
||||
LinkedList<DiscoveredExtension> sortedList = new LinkedList<>();
|
||||
|
||||
// entries with empty lists
|
||||
List<Map.Entry<DiscoveredExtension, List<DiscoveredExtension>>> loadableExtensions;
|
||||
// While there are entries with no more elements (no more dependencies)
|
||||
while (!(
|
||||
loadableExtensions = dependencyMap.entrySet().stream().filter(entry -> areAllDependenciesLoaded(entry.getValue())).collect(Collectors.toList())
|
||||
).isEmpty()
|
||||
) {
|
||||
// Get all "loadable" (not actually being loaded!) extensions and put them in the sorted list.
|
||||
for (Map.Entry<DiscoveredExtension, List<DiscoveredExtension>> entry : loadableExtensions) {
|
||||
// Add to sorted list.
|
||||
sortedList.add(entry.getKey());
|
||||
// Remove to make the next iterations a little bit quicker (hopefully) and to find cyclic dependencies.
|
||||
dependencyMap.remove(entry.getKey());
|
||||
// Remove this dependency from all the lists (if they include it) to make way for next level of extensions.
|
||||
dependencyMap.forEach((key, dependencyList) -> dependencyList.remove(entry.getKey()));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if there are cyclic extensions.
|
||||
if (!dependencyMap.isEmpty()) {
|
||||
LOGGER.error("Minestom found " + dependencyMap.size() + " cyclic extensions.");
|
||||
LOGGER.error("Cyclic extensions depend on each other and can therefore not be loaded.");
|
||||
for (Map.Entry<DiscoveredExtension, List<DiscoveredExtension>> entry : dependencyMap.entrySet()) {
|
||||
DiscoveredExtension discoveredExtension = entry.getKey();
|
||||
LOGGER.error(discoveredExtension.getName() + " could not be loaded, as it depends on: "
|
||||
+ entry.getValue().stream().map(DiscoveredExtension::getName).collect(Collectors.joining(", "))
|
||||
+ "."
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return sortedList;
|
||||
}
|
||||
|
||||
private boolean areAllDependenciesLoaded(List<DiscoveredExtension> dependencies) {
|
||||
return dependencies.isEmpty() || dependencies.stream().allMatch(ext -> extensions.containsKey(ext.getName().toLowerCase()));
|
||||
}
|
||||
|
||||
private void loadDependencies(List<DiscoveredExtension> extensions) {
|
||||
List<DiscoveredExtension> allLoadedExtensions = new LinkedList<>(extensions);
|
||||
extensionList.stream().map(ext -> ext.getDescription().getOrigin()).forEach(allLoadedExtensions::add);
|
||||
ExtensionDependencyResolver extensionDependencyResolver = new ExtensionDependencyResolver(allLoadedExtensions);
|
||||
for (DiscoveredExtension ext : extensions) {
|
||||
try {
|
||||
DependencyGetter getter = new DependencyGetter();
|
||||
DiscoveredExtension.ExternalDependencies externalDependencies = ext.getExternalDependencies();
|
||||
List<MavenRepository> repoList = new LinkedList<>();
|
||||
for (var repository : externalDependencies.repositories) {
|
||||
if (repository.name == null) {
|
||||
throw new IllegalStateException("Missing 'name' element in repository object.");
|
||||
}
|
||||
if (repository.name.isEmpty()) {
|
||||
throw new IllegalStateException("Invalid 'name' element in repository object.");
|
||||
}
|
||||
if (repository.url == null) {
|
||||
throw new IllegalStateException("Missing 'url' element in repository object.");
|
||||
}
|
||||
if (repository.url.isEmpty()) {
|
||||
throw new IllegalStateException("Invalid 'url' element in repository object.");
|
||||
}
|
||||
repoList.add(new MavenRepository(repository.name, repository.url));
|
||||
}
|
||||
getter.addMavenResolver(repoList);
|
||||
getter.addResolver(extensionDependencyResolver);
|
||||
|
||||
for (var artifact : externalDependencies.artifacts) {
|
||||
var resolved = getter.get(artifact, dependenciesFolder);
|
||||
addDependencyFile(resolved.getContentsLocation(), ext);
|
||||
LOGGER.trace("Dependency of extension {}: {}", ext.getName(), resolved);
|
||||
}
|
||||
|
||||
for (var dependencyName : ext.getDependencies()) {
|
||||
var resolved = getter.get(dependencyName, dependenciesFolder);
|
||||
addDependencyFile(resolved.getContentsLocation(), ext);
|
||||
LOGGER.trace("Dependency of extension {}: {}", ext.getName(), resolved);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
ext.loadStatus = DiscoveredExtension.LoadStatus.MISSING_DEPENDENCIES;
|
||||
LOGGER.error("Failed to load dependencies for extension {}", ext.getName());
|
||||
LOGGER.error("Extension '{}' will not be loaded", ext.getName());
|
||||
LOGGER.error("This is the exception", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addDependencyFile(URL dependency, DiscoveredExtension extension) {
|
||||
extension.files.add(dependency);
|
||||
LOGGER.trace("Added dependency {} to extension {} classpath", dependency.toExternalForm(), extension.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a URL into the classpath.
|
||||
* Creates a new class loader for the given extension.
|
||||
* Will add the new loader as a child of all its dependencies' loaders.
|
||||
*
|
||||
* @param urls {@link URL} (usually a JAR) that should be loaded.
|
||||
*/
|
||||
@NotNull
|
||||
public URLClassLoader newClassLoader(@NotNull URL[] urls) {
|
||||
return URLClassLoader.newInstance(urls, ExtensionManager.class.getClassLoader());
|
||||
public MinestomExtensionClassLoader newClassLoader(@NotNull DiscoveredExtension extension, @NotNull URL[] urls) {
|
||||
MinestomRootClassLoader root = MinestomRootClassLoader.getInstance();
|
||||
MinestomExtensionClassLoader loader = new MinestomExtensionClassLoader(extension.getName(), urls, root);
|
||||
if (extension.getDependencies().length == 0) {
|
||||
// orphaned extension, we can insert it directly
|
||||
root.addChild(loader);
|
||||
} else {
|
||||
// we need to keep track that it has actually been inserted
|
||||
// even though it should always be (due to the order in which extensions are loaders), it is an additional layer of """security"""
|
||||
boolean foundOne = false;
|
||||
for (String dependency : extension.getDependencies()) {
|
||||
if (extensionLoaders.containsKey(dependency.toLowerCase())) {
|
||||
MinestomExtensionClassLoader parentLoader = extensionLoaders.get(dependency.toLowerCase());
|
||||
parentLoader.addChild(loader);
|
||||
foundOne = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundOne) {
|
||||
LOGGER.error("Could not load extension {}, could not find any parent inside classloader hierarchy.", extension.getName());
|
||||
throw new RuntimeException("Could not load extension " + extension.getName() + ", could not find any parent inside classloader hierarchy.");
|
||||
}
|
||||
}
|
||||
return loader;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ -242,7 +431,7 @@ public final class ExtensionManager {
|
||||
|
||||
@NotNull
|
||||
public List<Extension> getExtensions() {
|
||||
return new ArrayList<>(extensions.values());
|
||||
return immutableExtensionListView;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@ -260,35 +449,177 @@ public final class ExtensionManager {
|
||||
*/
|
||||
private void setupCodeModifiers(@NotNull List<DiscoveredExtension> extensions) {
|
||||
final ClassLoader cl = getClass().getClassLoader();
|
||||
if (!(cl instanceof MinestomOverwriteClassLoader)) {
|
||||
if (!(cl instanceof MinestomRootClassLoader)) {
|
||||
LOGGER.warn("Current class loader is not a MinestomOverwriteClassLoader, but " + cl + ". This disables code modifiers (Mixin support is therefore disabled)");
|
||||
return;
|
||||
}
|
||||
MinestomOverwriteClassLoader modifiableClassLoader = (MinestomOverwriteClassLoader) cl;
|
||||
MinestomRootClassLoader modifiableClassLoader = (MinestomRootClassLoader) cl;
|
||||
LOGGER.info("Start loading code modifiers...");
|
||||
for (DiscoveredExtension extension : extensions) {
|
||||
try {
|
||||
if (extension.description.has("codeModifiers")) {
|
||||
final JsonArray codeModifierClasses = extension.description.getAsJsonArray("codeModifiers");
|
||||
for (JsonElement elem : codeModifierClasses) {
|
||||
modifiableClassLoader.loadModifier(extension.files, elem.getAsString());
|
||||
}
|
||||
for (String codeModifierClass : extension.getCodeModifiers()) {
|
||||
modifiableClassLoader.loadModifier(extension.files.toArray(new File[0]), codeModifierClass);
|
||||
}
|
||||
if (extension.description.has("mixinConfig")) {
|
||||
final String mixinConfigFile = extension.description.get("mixinConfig").getAsString();
|
||||
if (!extension.getMixinConfig().isEmpty()) {
|
||||
final String mixinConfigFile = extension.getMixinConfig();
|
||||
Mixins.addConfiguration(mixinConfigFile);
|
||||
LOGGER.info("Found mixin in extension " + extension.description.get("name").getAsString() + ": " + mixinConfigFile);
|
||||
LOGGER.info("Found mixin in extension " + extension.getName() + ": " + mixinConfigFile);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
LOGGER.error("Failed to load code modifier for extension in files: " + Arrays.toString(extension.files), e);
|
||||
LOGGER.error("Failed to load code modifier for extension in files: " + extension.files.stream().map(u -> u.toExternalForm()).collect(Collectors.joining(", ")), e);
|
||||
}
|
||||
}
|
||||
LOGGER.info("Done loading code modifiers.");
|
||||
}
|
||||
|
||||
private static class DiscoveredExtension {
|
||||
private File[] files;
|
||||
private JsonObject description;
|
||||
private void unload(Extension ext) {
|
||||
ext.preTerminate();
|
||||
ext.terminate();
|
||||
ext.postTerminate();
|
||||
ext.unload();
|
||||
|
||||
// remove as dependent of other extensions
|
||||
// this avoids issues where a dependent extension fails to reload, and prevents the base extension to reload too
|
||||
for (Extension e : extensionList) {
|
||||
e.getDescription().getDependents().remove(ext.getDescription().getName());
|
||||
}
|
||||
|
||||
String id = ext.getDescription().getName().toLowerCase();
|
||||
// remove from loaded extensions
|
||||
extensions.remove(id);
|
||||
extensionList.remove(ext);
|
||||
|
||||
// remove class loader, required to reload the classes
|
||||
MinestomExtensionClassLoader classloader = extensionLoaders.remove(id);
|
||||
try {
|
||||
// close resources
|
||||
classloader.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
MinestomRootClassLoader.getInstance().removeChildInHierarchy(classloader);
|
||||
}
|
||||
|
||||
public void reload(String extensionName) {
|
||||
Extension ext = extensions.get(extensionName.toLowerCase());
|
||||
if (ext == null) {
|
||||
throw new IllegalArgumentException("Extension " + extensionName + " is not currently loaded.");
|
||||
}
|
||||
|
||||
File originalJar = ext.getDescription().getOrigin().getOriginalJar();
|
||||
if (originalJar == null) {
|
||||
LOGGER.error("Cannot reload extension {} that is not from a .jar file!", extensionName);
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.info("Reload extension {} from jar file {}", extensionName, originalJar.getAbsolutePath());
|
||||
List<String> dependents = new LinkedList<>(ext.getDescription().getDependents()); // copy dependents list
|
||||
List<File> originalJarsOfDependents = new LinkedList<>();
|
||||
|
||||
for (String dependentID : dependents) {
|
||||
Extension dependentExt = extensions.get(dependentID.toLowerCase());
|
||||
File dependentOriginalJar = dependentExt.getDescription().getOrigin().getOriginalJar();
|
||||
originalJarsOfDependents.add(dependentOriginalJar);
|
||||
if (dependentOriginalJar == null) {
|
||||
LOGGER.error("Cannot reload extension {} that is not from a .jar file!", dependentID);
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.info("Unloading dependent extension {} (because it depends on {})", dependentID, extensionName);
|
||||
unload(dependentExt);
|
||||
}
|
||||
|
||||
LOGGER.info("Unloading extension {}", extensionName);
|
||||
unload(ext);
|
||||
|
||||
System.gc();
|
||||
|
||||
// ext and its dependents should no longer be referenced from now on
|
||||
|
||||
// rediscover extension to reload. We allow dependency changes, so we need to fully reload it
|
||||
List<DiscoveredExtension> extensionsToReload = new LinkedList<>();
|
||||
LOGGER.info("Rediscover extension {} from jar {}", extensionName, originalJar.getAbsolutePath());
|
||||
DiscoveredExtension rediscoveredExtension = discoverFromJar(originalJar);
|
||||
extensionsToReload.add(rediscoveredExtension);
|
||||
|
||||
for (File dependentJar : originalJarsOfDependents) {
|
||||
// rediscover dependent extension to reload
|
||||
LOGGER.info("Rediscover dependent extension (depends on {}) from jar {}", extensionName, dependentJar.getAbsolutePath());
|
||||
extensionsToReload.add(discoverFromJar(dependentJar));
|
||||
}
|
||||
|
||||
// ensure correct order of dependencies
|
||||
loadExtensionList(extensionsToReload);
|
||||
}
|
||||
|
||||
public boolean loadDynamicExtension(File jarFile) throws FileNotFoundException {
|
||||
if (!jarFile.exists()) {
|
||||
throw new FileNotFoundException("File '" + jarFile.getAbsolutePath() + "' does not exists. Cannot load extension.");
|
||||
}
|
||||
|
||||
LOGGER.info("Discover dynamic extension from jar {}", jarFile.getAbsolutePath());
|
||||
DiscoveredExtension discoveredExtension = discoverFromJar(jarFile);
|
||||
List<DiscoveredExtension> extensionsToLoad = Collections.singletonList(discoveredExtension);
|
||||
return loadExtensionList(extensionsToLoad);
|
||||
}
|
||||
|
||||
private boolean loadExtensionList(List<DiscoveredExtension> extensionsToLoad) {
|
||||
// ensure correct order of dependencies
|
||||
LOGGER.debug("Reorder extensions to ensure proper load order");
|
||||
extensionsToLoad = generateLoadOrder(extensionsToLoad);
|
||||
loadDependencies(extensionsToLoad);
|
||||
|
||||
// setup new classloaders for the extensions to reload
|
||||
for (DiscoveredExtension toReload : extensionsToLoad) {
|
||||
LOGGER.debug("Setting up classloader for extension {}", toReload.getName());
|
||||
setupClassLoader(toReload);
|
||||
}
|
||||
|
||||
// setup code modifiers for these extensions
|
||||
// TODO: it is possible the new modifiers cannot be applied (because the targeted classes are already loaded), should we issue a warning?
|
||||
setupCodeModifiers(extensionsToLoad);
|
||||
|
||||
List<Extension> newExtensions = new LinkedList<>();
|
||||
for (DiscoveredExtension toReload : extensionsToLoad) {
|
||||
// reload extensions
|
||||
LOGGER.info("Actually load extension {}", toReload.getName());
|
||||
Extension loadedExtension = attemptSingleLoad(toReload);
|
||||
if (loadedExtension != null) {
|
||||
newExtensions.add(loadedExtension);
|
||||
}
|
||||
}
|
||||
|
||||
if (newExtensions.isEmpty()) {
|
||||
LOGGER.error("No extensions to load, skipping callbacks");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGGER.info("Load complete, firing preinit, init and then postinit callbacks");
|
||||
// retrigger preinit, init and postinit
|
||||
newExtensions.forEach(Extension::preInitialize);
|
||||
newExtensions.forEach(Extension::initialize);
|
||||
newExtensions.forEach(Extension::postInitialize);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void unloadExtension(String extensionName) {
|
||||
Extension ext = extensions.get(extensionName.toLowerCase());
|
||||
if (ext == null) {
|
||||
throw new IllegalArgumentException("Extension " + extensionName + " is not currently loaded.");
|
||||
}
|
||||
List<String> dependents = new LinkedList<>(ext.getDescription().getDependents()); // copy dependents list
|
||||
|
||||
for (String dependentID : dependents) {
|
||||
Extension dependentExt = extensions.get(dependentID.toLowerCase());
|
||||
LOGGER.info("Unloading dependent extension {} (because it depends on {})", dependentID, extensionName);
|
||||
unload(dependentExt);
|
||||
}
|
||||
|
||||
LOGGER.info("Unloading extension {}", extensionName);
|
||||
unload(ext);
|
||||
|
||||
// call GC to try to get rid of classes and classloader
|
||||
System.gc();
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ package net.minestom.server.extras.selfmodification;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
|
||||
/**
|
||||
* Will be called by {@link MinestomOverwriteClassLoader} to transform classes at load-time
|
||||
* Will be called by {@link MinestomRootClassLoader} to transform classes at load-time
|
||||
*/
|
||||
public abstract class CodeModifier {
|
||||
/**
|
||||
|
@ -0,0 +1,41 @@
|
||||
package net.minestom.server.extras.selfmodification;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Classloader part of a hierarchy of classloader
|
||||
*/
|
||||
public abstract class HierarchyClassLoader extends URLClassLoader {
|
||||
protected final List<MinestomExtensionClassLoader> children = new LinkedList<>();
|
||||
|
||||
public HierarchyClassLoader(String name, URL[] urls, ClassLoader parent) {
|
||||
super(name, urls, parent);
|
||||
}
|
||||
|
||||
public void addChild(@NotNull MinestomExtensionClassLoader loader) {
|
||||
children.add(loader);
|
||||
}
|
||||
|
||||
public InputStream getResourceAsStreamWithChildren(String name) {
|
||||
InputStream in = getResourceAsStream(name);
|
||||
if(in != null) return in;
|
||||
|
||||
for(MinestomExtensionClassLoader child : children) {
|
||||
InputStream childInput = child.getResourceAsStreamWithChildren(name);
|
||||
if(childInput != null)
|
||||
return childInput;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void removeChildInHierarchy(MinestomExtensionClassLoader child) {
|
||||
children.remove(child);
|
||||
children.forEach(c -> c.removeChildInHierarchy(child));
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
package net.minestom.server.extras.selfmodification;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
|
||||
public class MinestomExtensionClassLoader extends HierarchyClassLoader {
|
||||
/**
|
||||
* Root ClassLoader, everything goes through it before any attempt at loading is done inside this classloader
|
||||
*/
|
||||
private final MinestomRootClassLoader root;
|
||||
|
||||
public MinestomExtensionClassLoader(String name, URL[] urls, MinestomRootClassLoader root) {
|
||||
super(name, urls, root);
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> loadClass(String name) throws ClassNotFoundException {
|
||||
return root.loadClass(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
return root.loadClass(name, resolve);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assumes the name is not null, nor it does represent a protected class
|
||||
* @param name
|
||||
* @return
|
||||
* @throws ClassNotFoundException if the class is not found inside this classloader
|
||||
*/
|
||||
public Class<?> loadClassAsChild(String name, boolean resolve) throws ClassNotFoundException {
|
||||
Class<?> loadedClass = findLoadedClass(name);
|
||||
if(loadedClass != null) {
|
||||
return loadedClass;
|
||||
}
|
||||
|
||||
try {
|
||||
// not in children, attempt load in this classloader
|
||||
String path = name.replace(".", "/") + ".class";
|
||||
InputStream in = getResourceAsStream(path);
|
||||
if (in == null) {
|
||||
throw new ClassNotFoundException("Could not load class " + name);
|
||||
}
|
||||
try (in) {
|
||||
byte[] bytes = in.readAllBytes();
|
||||
bytes = root.transformBytes(bytes, name);
|
||||
Class<?> clazz = defineClass(name, bytes, 0, bytes.length);
|
||||
if (resolve) {
|
||||
resolveClass(clazz);
|
||||
}
|
||||
return clazz;
|
||||
} catch (IOException e) {
|
||||
throw new ClassNotFoundException("Could not load class " + name, e);
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
for(MinestomExtensionClassLoader child : children) {
|
||||
try {
|
||||
Class<?> loaded = child.loadClassAsChild(name, resolve);
|
||||
return loaded;
|
||||
} catch (ClassNotFoundException e1) {
|
||||
// move on to next child
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
super.finalize();
|
||||
System.err.println("Class loader "+getName()+" finalized.");
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
package net.minestom.server.extras.selfmodification;
|
||||
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
@ -10,6 +9,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
@ -22,11 +22,11 @@ import java.util.Set;
|
||||
/**
|
||||
* Class Loader that can modify class bytecode when they are loaded
|
||||
*/
|
||||
public class MinestomOverwriteClassLoader extends URLClassLoader {
|
||||
public class MinestomRootClassLoader extends HierarchyClassLoader {
|
||||
|
||||
public final static Logger LOGGER = LoggerFactory.getLogger(MinecraftServer.class);
|
||||
public final static Logger LOGGER = LoggerFactory.getLogger(MinestomRootClassLoader.class);
|
||||
|
||||
private static MinestomOverwriteClassLoader INSTANCE;
|
||||
private static MinestomRootClassLoader INSTANCE;
|
||||
|
||||
/**
|
||||
* Classes that cannot be loaded/modified by this classloader.
|
||||
@ -47,6 +47,7 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
|
||||
add("org.apache");
|
||||
add("org.spongepowered");
|
||||
add("net.minestom.server.extras.selfmodification");
|
||||
add("org.jboss.shrinkwrap.resolver");
|
||||
}
|
||||
};
|
||||
/**
|
||||
@ -62,16 +63,16 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
|
||||
// TODO: priorities?
|
||||
private final List<CodeModifier> modifiers = new LinkedList<>();
|
||||
|
||||
private MinestomOverwriteClassLoader(ClassLoader parent) {
|
||||
super("Minestom ClassLoader", extractURLsFromClasspath(), parent);
|
||||
private MinestomRootClassLoader(ClassLoader parent) {
|
||||
super("Minestom Root ClassLoader", extractURLsFromClasspath(), parent);
|
||||
asmClassLoader = newChild(new URL[0]);
|
||||
}
|
||||
|
||||
public static MinestomOverwriteClassLoader getInstance() {
|
||||
public static MinestomRootClassLoader getInstance() {
|
||||
if (INSTANCE == null) {
|
||||
synchronized (MinestomOverwriteClassLoader.class) {
|
||||
synchronized (MinestomRootClassLoader.class) {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = new MinestomOverwriteClassLoader(MinestomOverwriteClassLoader.class.getClassLoader());
|
||||
INSTANCE = new MinestomRootClassLoader(MinestomRootClassLoader.class.getClassLoader());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -117,7 +118,7 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
|
||||
return super.loadClass(name, resolve);
|
||||
}
|
||||
|
||||
return define(name, loadBytes(name, true), resolve);
|
||||
return define(name, resolve);
|
||||
} catch (Exception ex) {
|
||||
LOGGER.trace("Fail to load class, resorting to parent loader: " + name, ex);
|
||||
// fail to load class, let parent load
|
||||
@ -138,13 +139,29 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
|
||||
return true;
|
||||
}
|
||||
|
||||
private Class<?> define(String name, byte[] bytes, boolean resolve) {
|
||||
Class<?> defined = defineClass(name, bytes, 0, bytes.length);
|
||||
LOGGER.trace("Loaded with code modifiers: " + name);
|
||||
if (resolve) {
|
||||
resolveClass(defined);
|
||||
private Class<?> define(String name, boolean resolve) throws IOException, ClassNotFoundException {
|
||||
try {
|
||||
byte[] bytes = loadBytes(name, true);
|
||||
Class<?> defined = defineClass(name, bytes, 0, bytes.length);
|
||||
LOGGER.trace("Loaded with code modifiers: " + name);
|
||||
if (resolve) {
|
||||
resolveClass(defined);
|
||||
}
|
||||
return defined;
|
||||
} catch (ClassNotFoundException e) {
|
||||
// could not load inside this classloader, attempt with children
|
||||
Class<?> defined = null;
|
||||
for(MinestomExtensionClassLoader subloader : children) {
|
||||
try {
|
||||
defined = subloader.loadClassAsChild(name, resolve);
|
||||
LOGGER.trace("Loaded from child {}: {}", subloader, name);
|
||||
return defined;
|
||||
} catch (ClassNotFoundException e1) {
|
||||
// not found inside this child, move on to next
|
||||
}
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return defined;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -160,9 +177,35 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
|
||||
if (name == null)
|
||||
throw new ClassNotFoundException();
|
||||
String path = name.replace(".", "/") + ".class";
|
||||
byte[] bytes = getResourceAsStream(path).readAllBytes();
|
||||
if (transform && !isProtected(name)) {
|
||||
ClassReader reader = new ClassReader(bytes);
|
||||
InputStream input = getResourceAsStream(path);
|
||||
if(input == null) {
|
||||
throw new ClassNotFoundException("Could not find resource "+path);
|
||||
}
|
||||
byte[] originalBytes = input.readAllBytes();
|
||||
if(transform) {
|
||||
return transformBytes(originalBytes, name);
|
||||
}
|
||||
return originalBytes;
|
||||
}
|
||||
|
||||
public byte[] loadBytesWithChildren(String name, boolean transform) throws IOException, ClassNotFoundException {
|
||||
if (name == null)
|
||||
throw new ClassNotFoundException();
|
||||
String path = name.replace(".", "/") + ".class";
|
||||
InputStream input = getResourceAsStreamWithChildren(path);
|
||||
if(input == null) {
|
||||
throw new ClassNotFoundException("Could not find resource "+path);
|
||||
}
|
||||
byte[] originalBytes = input.readAllBytes();
|
||||
if(transform) {
|
||||
return transformBytes(originalBytes, name);
|
||||
}
|
||||
return originalBytes;
|
||||
}
|
||||
|
||||
byte[] transformBytes(byte[] classBytecode, String name) {
|
||||
if (!isProtected(name)) {
|
||||
ClassReader reader = new ClassReader(classBytecode);
|
||||
ClassNode node = new ClassNode();
|
||||
reader.accept(node, 0);
|
||||
boolean modified = false;
|
||||
@ -182,11 +225,11 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
|
||||
}
|
||||
};
|
||||
node.accept(writer);
|
||||
bytes = writer.toByteArray();
|
||||
classBytecode = writer.toByteArray();
|
||||
LOGGER.trace("Modified " + name);
|
||||
}
|
||||
}
|
||||
return bytes;
|
||||
return classBytecode;
|
||||
}
|
||||
|
||||
// overriden to increase access (from protected to public)
|
||||
@ -226,6 +269,11 @@ public class MinestomOverwriteClassLoader extends URLClassLoader {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addURL(URL url) {
|
||||
super.addURL(url);
|
||||
}
|
||||
|
||||
public List<CodeModifier> getModifiers() {
|
||||
return modifiers;
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package net.minestom.server.extras.selfmodification.mixins;
|
||||
|
||||
import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader;
|
||||
import net.minestom.server.extras.selfmodification.MinestomRootClassLoader;
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.tree.ClassNode;
|
||||
import org.spongepowered.asm.service.IClassBytecodeProvider;
|
||||
@ -11,9 +11,9 @@ import java.io.IOException;
|
||||
* Provides class bytecode for Mixin
|
||||
*/
|
||||
public class MinestomBytecodeProvider implements IClassBytecodeProvider {
|
||||
private final MinestomOverwriteClassLoader classLoader;
|
||||
private final MinestomRootClassLoader classLoader;
|
||||
|
||||
public MinestomBytecodeProvider(MinestomOverwriteClassLoader classLoader) {
|
||||
public MinestomBytecodeProvider(MinestomRootClassLoader classLoader) {
|
||||
this.classLoader = classLoader;
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ public class MinestomBytecodeProvider implements IClassBytecodeProvider {
|
||||
ClassNode node = new ClassNode();
|
||||
ClassReader reader;
|
||||
try {
|
||||
reader = new ClassReader(classLoader.loadBytes(name, transform));
|
||||
reader = new ClassReader(classLoader.loadBytesWithChildren(name, transform));
|
||||
} catch (IOException e) {
|
||||
throw new ClassNotFoundException("Could not load ClassNode with name " + name, e);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
package net.minestom.server.extras.selfmodification.mixins;
|
||||
|
||||
import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader;
|
||||
import net.minestom.server.extras.selfmodification.MinestomRootClassLoader;
|
||||
import org.spongepowered.asm.service.IClassProvider;
|
||||
|
||||
import java.net.URL;
|
||||
@ -9,9 +9,9 @@ import java.net.URL;
|
||||
* Provides classes for Mixin
|
||||
*/
|
||||
public class MinestomClassProvider implements IClassProvider {
|
||||
private final MinestomOverwriteClassLoader classLoader;
|
||||
private final MinestomRootClassLoader classLoader;
|
||||
|
||||
public MinestomClassProvider(MinestomOverwriteClassLoader classLoader) {
|
||||
public MinestomClassProvider(MinestomRootClassLoader classLoader) {
|
||||
this.classLoader = classLoader;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
package net.minestom.server.extras.selfmodification.mixins;
|
||||
|
||||
import net.minestom.server.extras.selfmodification.MinestomOverwriteClassLoader;
|
||||
import net.minestom.server.extras.selfmodification.MinestomRootClassLoader;
|
||||
import org.spongepowered.asm.launch.platform.container.ContainerHandleVirtual;
|
||||
import org.spongepowered.asm.launch.platform.container.IContainerHandle;
|
||||
import org.spongepowered.asm.mixin.MixinEnvironment;
|
||||
@ -13,7 +13,7 @@ import java.util.Collections;
|
||||
|
||||
public class MixinServiceMinestom extends MixinServiceAbstract {
|
||||
|
||||
private final MinestomOverwriteClassLoader classLoader;
|
||||
private final MinestomRootClassLoader classLoader;
|
||||
private final MinestomClassProvider classProvider;
|
||||
private final MinestomBytecodeProvider bytecodeProvider;
|
||||
private final MixinAuditTrailMinestom auditTrail;
|
||||
@ -22,7 +22,7 @@ public class MixinServiceMinestom extends MixinServiceAbstract {
|
||||
|
||||
public MixinServiceMinestom() {
|
||||
INSTANCE = this;
|
||||
this.classLoader = MinestomOverwriteClassLoader.getInstance();
|
||||
this.classLoader = MinestomRootClassLoader.getInstance();
|
||||
classProvider = new MinestomClassProvider(classLoader);
|
||||
bytecodeProvider = new MinestomBytecodeProvider(classLoader);
|
||||
auditTrail = new MixinAuditTrailMinestom();
|
||||
@ -65,7 +65,7 @@ public class MixinServiceMinestom extends MixinServiceAbstract {
|
||||
|
||||
@Override
|
||||
public InputStream getResourceAsStream(String name) {
|
||||
return classLoader.getResourceAsStream(name);
|
||||
return classLoader.getResourceAsStreamWithChildren(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -11,6 +11,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@ -93,8 +94,13 @@ public final class PacketListenerManager {
|
||||
* @return true if the packet is not cancelled, false otherwise
|
||||
*/
|
||||
public <T extends ServerPacket> boolean processServerPacket(@NotNull T packet, @NotNull Player player) {
|
||||
final List<PacketConsumer<ServerPacket>> consumers = CONNECTION_MANAGER.getSendPacketConsumers();
|
||||
if (consumers.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final PacketController packetController = new PacketController();
|
||||
for (PacketConsumer<ServerPacket> packetConsumer : CONNECTION_MANAGER.getSendPacketConsumers()) {
|
||||
for (PacketConsumer<ServerPacket> packetConsumer : consumers) {
|
||||
packetConsumer.accept(player, packetController, packet);
|
||||
}
|
||||
|
||||
|
@ -123,10 +123,10 @@ public enum MapColors {
|
||||
|
||||
// From the wiki: https://minecraft.gamepedia.com/Map_item_format
|
||||
// Map Color ID Multiply R,G,B By = Multiplier
|
||||
//Base Color ID×4 + 0 180 0.71
|
||||
//Base Color ID×4 + 1 220 0.86
|
||||
//Base Color ID×4 + 2 255 (same color) 1
|
||||
//Base Color ID×4 + 3 135 0.53
|
||||
//Base Color ID*4 + 0 180 0.71
|
||||
//Base Color ID*4 + 1 220 0.86
|
||||
//Base Color ID*4 + 2 255 (same color) 1
|
||||
//Base Color ID*4 + 3 135 0.53
|
||||
|
||||
/**
|
||||
* Returns the color index with RGB multiplied by 0.53, to use on a map
|
||||
|
@ -145,11 +145,11 @@ public final class ConnectionManager {
|
||||
/**
|
||||
* Gets all the listeners which are called for each packet received.
|
||||
*
|
||||
* @return an unmodifiable list of packet's consumers
|
||||
* @return a list of packet's consumers
|
||||
*/
|
||||
@NotNull
|
||||
public List<PacketConsumer<ClientPlayPacket>> getReceivePacketConsumers() {
|
||||
return Collections.unmodifiableList(receivePacketConsumers);
|
||||
return receivePacketConsumers;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -164,7 +164,7 @@ public final class ConnectionManager {
|
||||
/**
|
||||
* Gets all the listeners which are called for each packet sent.
|
||||
*
|
||||
* @return an unmodifiable list of packet's consumers
|
||||
* @return a list of packet's consumers
|
||||
*/
|
||||
@NotNull
|
||||
public List<PacketConsumer<ServerPacket>> getSendPacketConsumers() {
|
||||
|
@ -12,7 +12,8 @@ import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.channel.socket.ServerSocketChannel;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.channel.socket.nio.NioServerSocketChannel;
|
||||
import io.netty.handler.traffic.ChannelTrafficShapingHandler;
|
||||
import io.netty.handler.traffic.GlobalChannelTrafficShapingHandler;
|
||||
import io.netty.handler.traffic.TrafficCounter;
|
||||
import net.minestom.server.network.PacketProcessor;
|
||||
import net.minestom.server.network.netty.channel.ClientChannel;
|
||||
import net.minestom.server.network.netty.codec.LegacyPingHandler;
|
||||
@ -22,9 +23,14 @@ import net.minestom.server.network.netty.codec.PacketFramer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
public class NettyServer {
|
||||
|
||||
private static final long DEFAULT_CHANNEL_WRITE_LIMIT = 600_000L;
|
||||
private static final long DEFAULT_CHANNEL_READ_LIMIT = 100_000L;
|
||||
|
||||
private final EventLoopGroup boss, worker;
|
||||
private final ServerBootstrap bootstrap;
|
||||
|
||||
@ -33,9 +39,12 @@ public class NettyServer {
|
||||
private String address;
|
||||
private int port;
|
||||
|
||||
// Options
|
||||
private long writeLimit;
|
||||
private long readLimit;
|
||||
private final GlobalChannelTrafficShapingHandler globalTrafficHandler;
|
||||
|
||||
/**
|
||||
* Scheduler used by {@code globalTrafficHandler}.
|
||||
*/
|
||||
private final ScheduledExecutorService trafficScheduler = Executors.newScheduledThreadPool(1);
|
||||
|
||||
public NettyServer(@NotNull PacketProcessor packetProcessor) {
|
||||
Class<? extends ServerChannel> channel;
|
||||
@ -61,19 +70,30 @@ public class NettyServer {
|
||||
.group(boss, worker)
|
||||
.channel(channel);
|
||||
|
||||
this.globalTrafficHandler = new GlobalChannelTrafficShapingHandler(trafficScheduler, 200) {
|
||||
@Override
|
||||
protected void doAccounting(TrafficCounter counter) {
|
||||
// TODO proper monitoring API
|
||||
//System.out.println("data " + counter.lastWriteThroughput() / 1000 + " " + counter.lastReadThroughput() / 1000);
|
||||
}
|
||||
};
|
||||
|
||||
globalTrafficHandler.setWriteChannelLimit(DEFAULT_CHANNEL_WRITE_LIMIT);
|
||||
globalTrafficHandler.setReadChannelLimit(DEFAULT_CHANNEL_READ_LIMIT);
|
||||
|
||||
|
||||
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
|
||||
protected void initChannel(@NotNull SocketChannel ch) {
|
||||
ChannelConfig config = ch.config();
|
||||
config.setOption(ChannelOption.TCP_NODELAY, true);
|
||||
config.setOption(ChannelOption.SO_SNDBUF, 1_000_000);
|
||||
|
||||
ChannelPipeline pipeline = ch.pipeline();
|
||||
|
||||
ChannelTrafficShapingHandler channelTrafficShapingHandler =
|
||||
new ChannelTrafficShapingHandler(writeLimit, readLimit, 200);
|
||||
|
||||
pipeline.addLast("traffic-limiter", channelTrafficShapingHandler);
|
||||
pipeline.addLast("traffic-limiter", globalTrafficHandler);
|
||||
|
||||
// First check should verify if the packet is a legacy ping (from 1.6 version and earlier)
|
||||
// Removed from the pipeline later in LegacyPingHandler if unnecessary (>1.6)
|
||||
pipeline.addLast("legacy-ping", new LegacyPingHandler());
|
||||
|
||||
// Adds packetLength at start | Reads framed bytebuf
|
||||
@ -90,7 +110,13 @@ public class NettyServer {
|
||||
});
|
||||
}
|
||||
|
||||
public void start(String address, int port) {
|
||||
/**
|
||||
* Binds the address to start the server.
|
||||
*
|
||||
* @param address the server address
|
||||
* @param port the server port
|
||||
*/
|
||||
public void start(@NotNull String address, int port) {
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
|
||||
@ -126,58 +152,27 @@ public class NettyServer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the server write limit.
|
||||
* Gets the traffic handler, used to control channel and global bandwidth.
|
||||
* <p>
|
||||
* Used when you want to limit the bandwidth used by a single connection.
|
||||
* Can also prevent the networking threads from being unresponsive.
|
||||
* The object can be modified as specified by Netty documentation.
|
||||
*
|
||||
* @return the write limit in bytes
|
||||
* @return the global traffic handler
|
||||
*/
|
||||
public long getWriteLimit() {
|
||||
return writeLimit;
|
||||
@NotNull
|
||||
public GlobalChannelTrafficShapingHandler getGlobalTrafficHandler() {
|
||||
return globalTrafficHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the server write limit
|
||||
* <p>
|
||||
* WARNING: the change will only apply to new connections, the current ones will not be updated.
|
||||
*
|
||||
* @param writeLimit the new write limit in bytes, 0 to disable
|
||||
* @see #getWriteLimit()
|
||||
* Stops the server and the various services.
|
||||
*/
|
||||
public void setWriteLimit(long writeLimit) {
|
||||
this.writeLimit = writeLimit;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the server read limit.
|
||||
* <p>
|
||||
* Used when you want to limit the bandwidth used by a single connection.
|
||||
* Can also prevent the networking threads from being unresponsive.
|
||||
*
|
||||
* @return the read limit in bytes
|
||||
*/
|
||||
public long getReadLimit() {
|
||||
return readLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the server read limit
|
||||
* <p>
|
||||
* WARNING: the change will only apply to new connections, the current ones will not be updated.
|
||||
*
|
||||
* @param readLimit the new read limit in bytes, 0 to disable
|
||||
* @see #getWriteLimit()
|
||||
*/
|
||||
public void setReadLimit(long readLimit) {
|
||||
this.readLimit = readLimit;
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
serverChannel.close();
|
||||
this.serverChannel.close();
|
||||
|
||||
worker.shutdownGracefully();
|
||||
boss.shutdownGracefully();
|
||||
this.worker.shutdownGracefully();
|
||||
this.boss.shutdownGracefully();
|
||||
|
||||
this.trafficScheduler.shutdown();
|
||||
this.globalTrafficHandler.release();
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,8 @@ public class PacketDecoder extends ByteToMessageDecoder {
|
||||
@Override
|
||||
protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> list) {
|
||||
if (buf.readableBytes() > 0) {
|
||||
list.add(new InboundPacket(Utils.readVarInt(buf), buf));
|
||||
final int packetId = Utils.readVarInt(buf);
|
||||
list.add(new InboundPacket(packetId, buf));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,8 +56,16 @@ public class NettyPlayerConnection extends PlayerConnection {
|
||||
this.remoteAddress = channel.remoteAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update() {
|
||||
// Flush
|
||||
this.channel.eventLoop().execute(() -> channel.flush());
|
||||
// Network stats
|
||||
super.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the encryption key and add the channels to the pipeline.
|
||||
* Sets the encryption key and add the codecs to the pipeline.
|
||||
*
|
||||
* @param secretKey the secret key to use in the encryption
|
||||
* @throws IllegalStateException if encryption is already enabled for this connection
|
||||
@ -65,12 +73,12 @@ public class NettyPlayerConnection extends PlayerConnection {
|
||||
public void setEncryptionKey(@NotNull SecretKey secretKey) {
|
||||
Check.stateCondition(encrypted, "Encryption is already enabled!");
|
||||
this.encrypted = true;
|
||||
getChannel().pipeline().addBefore("framer", "decrypt", new Decrypter(MojangCrypt.getCipher(2, secretKey)));
|
||||
getChannel().pipeline().addBefore("framer", "encrypt", new Encrypter(MojangCrypt.getCipher(1, secretKey)));
|
||||
channel.pipeline().addBefore("framer", "decrypt", new Decrypter(MojangCrypt.getCipher(2, secretKey)));
|
||||
channel.pipeline().addBefore("framer", "encrypt", new Encrypter(MojangCrypt.getCipher(1, secretKey)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables compression and add a new channel to the pipeline.
|
||||
* Enables compression and add a new codec to the pipeline.
|
||||
*
|
||||
* @param threshold the threshold for a packet to be compressible
|
||||
* @throws IllegalStateException if encryption is already enabled for this connection
|
||||
@ -93,9 +101,9 @@ public class NettyPlayerConnection extends PlayerConnection {
|
||||
public void sendPacket(@NotNull ServerPacket serverPacket) {
|
||||
if (shouldSendPacket(serverPacket)) {
|
||||
if (getPlayer() != null) {
|
||||
channel.write(serverPacket); // Flush on player update
|
||||
this.channel.write(serverPacket); // Flush on player update
|
||||
} else {
|
||||
channel.writeAndFlush(serverPacket);
|
||||
this.channel.writeAndFlush(serverPacket);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ public abstract class PlayerConnection {
|
||||
/**
|
||||
* Updates values related to the network connection.
|
||||
*/
|
||||
public void updateStats() {
|
||||
public void update() {
|
||||
// Check rate limit
|
||||
if (MinecraftServer.getRateLimit() > 0) {
|
||||
tickCounter++;
|
||||
|
@ -93,9 +93,9 @@ public class StorageLocation {
|
||||
Check.notNull(dataType, "You can only save registered DataType type!");
|
||||
|
||||
// Encode the data
|
||||
BinaryWriter binaryWriter = new BinaryWriter();
|
||||
dataType.encode(binaryWriter, object); // Encode
|
||||
final byte[] encodedValue = binaryWriter.toByteArray(); // Retrieve bytes
|
||||
BinaryWriter writer = new BinaryWriter();
|
||||
dataType.encode(writer, object); // Encode
|
||||
final byte[] encodedValue = writer.toByteArray(); // Retrieve bytes
|
||||
|
||||
// Write it
|
||||
set(key, encodedValue);
|
||||
|
@ -74,6 +74,7 @@ public final class PacketUtils {
|
||||
private static void writePacket(@NotNull ByteBuf buf, @NotNull ByteBuf packetBuffer, int packetId) {
|
||||
Utils.writeVarIntBuf(buf, packetId);
|
||||
buf.writeBytes(packetBuffer);
|
||||
packetBuffer.release();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7,6 +7,7 @@ import net.minestom.server.utils.BlockPosition;
|
||||
import net.minestom.server.utils.NBTUtils;
|
||||
import net.minestom.server.utils.SerializerUtils;
|
||||
import net.minestom.server.utils.Utils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jglrxavpok.hephaistos.nbt.NBT;
|
||||
import org.jglrxavpok.hephaistos.nbt.NBTWriter;
|
||||
|
||||
@ -34,12 +35,21 @@ public class BinaryWriter extends OutputStream {
|
||||
this.buffer = Unpooled.buffer(initialCapacity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link BinaryWriter} from multiple a single buffer.
|
||||
*
|
||||
* @param buffer the writer buffer
|
||||
*/
|
||||
public BinaryWriter(@NotNull ByteBuf buffer) {
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link BinaryWriter} from multiple buffers.
|
||||
*
|
||||
* @param buffers the buffers making this
|
||||
*/
|
||||
public BinaryWriter(ByteBuf... buffers) {
|
||||
public BinaryWriter(@NotNull ByteBuf... buffers) {
|
||||
this.buffer = Unpooled.wrappedBuffer(buffers);
|
||||
}
|
||||
|
||||
|
@ -36,6 +36,9 @@ public class Main {
|
||||
commandManager.register(new DimensionCommand());
|
||||
commandManager.register(new ShutdownCommand());
|
||||
commandManager.register(new TeleportCommand());
|
||||
commandManager.register(new ReloadExtensionCommand());
|
||||
commandManager.register(new UnloadExtensionCommand());
|
||||
commandManager.register(new LoadExtensionCommand());
|
||||
|
||||
commandManager.setUnknownCommandCallback((sender, command) -> sender.sendMessage("unknown command"));
|
||||
|
||||
|
@ -6,7 +6,6 @@ import net.minestom.server.instance.*;
|
||||
import net.minestom.server.instance.batch.ChunkBatch;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import net.minestom.server.network.ConnectionManager;
|
||||
import net.minestom.server.network.netty.NettyServer;
|
||||
import net.minestom.server.utils.Position;
|
||||
import net.minestom.server.world.biomes.Biome;
|
||||
|
||||
@ -37,14 +36,6 @@ public class MainDemo {
|
||||
});
|
||||
});
|
||||
|
||||
// OPTIONAL: optimize networking to prevent having unresponsive threads
|
||||
{
|
||||
NettyServer nettyServer = MinecraftServer.getNettyServer();
|
||||
// Set the maximum bandwidth out and in to 500KB/s, largely enough for a single client
|
||||
nettyServer.setWriteLimit(500_000);
|
||||
nettyServer.setReadLimit(500_000);
|
||||
}
|
||||
|
||||
// Start the server
|
||||
minecraftServer.start("localhost", 25565);
|
||||
}
|
||||
|
77
src/test/java/demo/commands/LoadExtensionCommand.java
Normal file
77
src/test/java/demo/commands/LoadExtensionCommand.java
Normal file
@ -0,0 +1,77 @@
|
||||
package demo.commands;
|
||||
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.command.CommandSender;
|
||||
import net.minestom.server.command.builder.Arguments;
|
||||
import net.minestom.server.command.builder.Command;
|
||||
import net.minestom.server.command.builder.arguments.Argument;
|
||||
import net.minestom.server.command.builder.arguments.ArgumentType;
|
||||
import net.minestom.server.extensions.ExtensionManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class LoadExtensionCommand extends Command {
|
||||
public LoadExtensionCommand() {
|
||||
super("load");
|
||||
|
||||
setDefaultExecutor(this::usage);
|
||||
|
||||
Argument extension = ArgumentType.DynamicStringArray("extensionName");
|
||||
|
||||
setArgumentCallback(this::gameModeCallback, extension);
|
||||
|
||||
addSyntax(this::execute, extension);
|
||||
}
|
||||
|
||||
private void usage(CommandSender sender, Arguments arguments) {
|
||||
sender.sendMessage("Usage: /load <extension file name>");
|
||||
}
|
||||
|
||||
private void execute(CommandSender sender, Arguments arguments) {
|
||||
String name = join(arguments.getStringArray("extensionName"));
|
||||
sender.sendMessage("extensionFile = "+name+"....");
|
||||
|
||||
ExtensionManager extensionManager = MinecraftServer.getExtensionManager();
|
||||
Path extensionFolder = extensionManager.getExtensionFolder().toPath().toAbsolutePath();
|
||||
Path extensionJar = extensionFolder.resolve(name);
|
||||
try {
|
||||
if(!extensionJar.toFile().getCanonicalPath().startsWith(extensionFolder.toFile().getCanonicalPath())) {
|
||||
sender.sendMessage("File name '"+name+"' does not represent a file inside the extensions folder. Will not load");
|
||||
return;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
sender.sendMessage("Failed to load extension: "+e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean managed = extensionManager.loadDynamicExtension(extensionJar.toFile());
|
||||
if(managed) {
|
||||
sender.sendMessage("Extension loaded!");
|
||||
} else {
|
||||
sender.sendMessage("Failed to load extension, check your logs.");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
sender.sendMessage("Failed to load extension: "+e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void gameModeCallback(CommandSender sender, String extension, int error) {
|
||||
sender.sendMessage("'" + extension + "' is not a valid extension name!");
|
||||
}
|
||||
|
||||
private String join(String[] extensionNameParts) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
for (int i = 0; i < extensionNameParts.length; i++) {
|
||||
String s = extensionNameParts[i];
|
||||
if(i != 0) {
|
||||
b.append(" ");
|
||||
}
|
||||
b.append(s);
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
}
|
98
src/test/java/demo/commands/ReloadExtensionCommand.java
Normal file
98
src/test/java/demo/commands/ReloadExtensionCommand.java
Normal file
@ -0,0 +1,98 @@
|
||||
package demo.commands;
|
||||
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.command.CommandSender;
|
||||
import net.minestom.server.command.builder.Arguments;
|
||||
import net.minestom.server.command.builder.Command;
|
||||
import net.minestom.server.command.builder.arguments.Argument;
|
||||
import net.minestom.server.command.builder.arguments.ArgumentType;
|
||||
import net.minestom.server.extensions.Extension;
|
||||
import net.minestom.server.extensions.ExtensionManager;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ReloadExtensionCommand extends Command {
|
||||
|
||||
// the extensions name as an array
|
||||
private static String[] extensionsName;
|
||||
|
||||
static {
|
||||
List<String> extensionsName = MinecraftServer.getExtensionManager().getExtensions()
|
||||
.stream()
|
||||
.map(extension -> extension.getDescription().getName())
|
||||
.collect(Collectors.toList());
|
||||
ReloadExtensionCommand.extensionsName = extensionsName.toArray(new String[0]);
|
||||
}
|
||||
|
||||
public ReloadExtensionCommand() {
|
||||
super("reload");
|
||||
|
||||
setDefaultExecutor(this::usage);
|
||||
|
||||
Argument extension = ArgumentType.DynamicStringArray("extensionName");
|
||||
|
||||
setArgumentCallback(this::gameModeCallback, extension);
|
||||
|
||||
addSyntax(this::execute, extension);
|
||||
}
|
||||
|
||||
private void usage(CommandSender sender, Arguments arguments) {
|
||||
sender.sendMessage("Usage: /reload <extension name>");
|
||||
}
|
||||
|
||||
private void execute(CommandSender sender, Arguments arguments) {
|
||||
String name = join(arguments.getStringArray("extensionName"));
|
||||
sender.sendMessage("extensionName = " + name + "....");
|
||||
|
||||
ExtensionManager extensionManager = MinecraftServer.getExtensionManager();
|
||||
Extension ext = extensionManager.getExtension(name);
|
||||
if (ext != null) {
|
||||
try {
|
||||
extensionManager.reload(name);
|
||||
} catch (Throwable t) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
t.printStackTrace();
|
||||
t.printStackTrace(new PrintStream(baos));
|
||||
baos.flush();
|
||||
baos.close();
|
||||
String contents = new String(baos.toByteArray(), StandardCharsets.UTF_8);
|
||||
contents.lines().forEach(sender::sendMessage);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sender.sendMessage("Extension '" + name + "' does not exist.");
|
||||
}
|
||||
}
|
||||
|
||||
private void gameModeCallback(CommandSender sender, String extension, int error) {
|
||||
sender.sendMessage("'" + extension + "' is not a valid extension name!");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String[] onDynamicWrite(@NotNull String text) {
|
||||
return extensionsName;
|
||||
}
|
||||
|
||||
private String join(String[] extensionNameParts) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
for (int i = 0; i < extensionNameParts.length; i++) {
|
||||
String s = extensionNameParts[i];
|
||||
if (i != 0) {
|
||||
b.append(" ");
|
||||
}
|
||||
b.append(s);
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import net.minestom.server.command.builder.Arguments;
|
||||
import net.minestom.server.command.builder.Command;
|
||||
import net.minestom.server.command.builder.arguments.Argument;
|
||||
import net.minestom.server.command.builder.arguments.ArgumentType;
|
||||
import net.minestom.server.entity.Player;
|
||||
|
||||
public class TestCommand extends Command {
|
||||
|
||||
@ -19,11 +20,12 @@ public class TestCommand extends Command {
|
||||
});
|
||||
|
||||
setDefaultExecutor((source, args) -> {
|
||||
System.out.println("DEFAULT");
|
||||
System.gc();
|
||||
source.sendMessage("Explicit GC executed!");
|
||||
});
|
||||
|
||||
addSyntax((source, args) -> {
|
||||
Player player = (Player) source;
|
||||
System.out.println("ARG 1");
|
||||
}, test);
|
||||
}
|
||||
|
76
src/test/java/demo/commands/UnloadExtensionCommand.java
Normal file
76
src/test/java/demo/commands/UnloadExtensionCommand.java
Normal file
@ -0,0 +1,76 @@
|
||||
package demo.commands;
|
||||
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.command.CommandSender;
|
||||
import net.minestom.server.command.builder.Arguments;
|
||||
import net.minestom.server.command.builder.Command;
|
||||
import net.minestom.server.command.builder.arguments.Argument;
|
||||
import net.minestom.server.command.builder.arguments.ArgumentType;
|
||||
import net.minestom.server.extensions.Extension;
|
||||
import net.minestom.server.extensions.ExtensionManager;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class UnloadExtensionCommand extends Command {
|
||||
public UnloadExtensionCommand() {
|
||||
super("unload");
|
||||
|
||||
setDefaultExecutor(this::usage);
|
||||
|
||||
Argument extension = ArgumentType.DynamicStringArray("extensionName");
|
||||
|
||||
setArgumentCallback(this::gameModeCallback, extension);
|
||||
|
||||
addSyntax(this::execute, extension);
|
||||
}
|
||||
|
||||
private void usage(CommandSender sender, Arguments arguments) {
|
||||
sender.sendMessage("Usage: /unload <extension name>");
|
||||
}
|
||||
|
||||
private void execute(CommandSender sender, Arguments arguments) {
|
||||
String name = join(arguments.getStringArray("extensionName"));
|
||||
sender.sendMessage("extensionName = "+name+"....");
|
||||
|
||||
ExtensionManager extensionManager = MinecraftServer.getExtensionManager();
|
||||
Extension ext = extensionManager.getExtension(name);
|
||||
if(ext != null) {
|
||||
try {
|
||||
extensionManager.unloadExtension(name);
|
||||
} catch (Throwable t) {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
t.printStackTrace();
|
||||
t.printStackTrace(new PrintStream(baos));
|
||||
baos.flush();
|
||||
baos.close();
|
||||
String contents = new String(baos.toByteArray(), StandardCharsets.UTF_8);
|
||||
contents.lines().forEach(sender::sendMessage);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sender.sendMessage("Extension '"+name+"' does not exist.");
|
||||
}
|
||||
}
|
||||
|
||||
private void gameModeCallback(CommandSender sender, String extension, int error) {
|
||||
sender.sendMessage("'" + extension + "' is not a valid extension name!");
|
||||
}
|
||||
|
||||
private String join(String[] extensionNameParts) {
|
||||
StringBuilder b = new StringBuilder();
|
||||
for (int i = 0; i < extensionNameParts.length; i++) {
|
||||
String s = extensionNameParts[i];
|
||||
if(i != 0) {
|
||||
b.append(" ");
|
||||
}
|
||||
b.append(s);
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
}
|
11
src/test/java/testextension/TestDemoLauncher.java
Normal file
11
src/test/java/testextension/TestDemoLauncher.java
Normal file
@ -0,0 +1,11 @@
|
||||
package testextension;
|
||||
|
||||
import net.minestom.server.Bootstrap;
|
||||
|
||||
public class TestDemoLauncher {
|
||||
|
||||
public static void main(String[] args) {
|
||||
Bootstrap.bootstrap("demo.Main", args);
|
||||
}
|
||||
|
||||
}
|
@ -11,7 +11,7 @@ public class TestExtensionLauncherArgs {
|
||||
System.arraycopy(args, 0, argsWithMixins, 0, args.length);
|
||||
argsWithMixins[argsWithMixins.length-2] = "--mixin";
|
||||
argsWithMixins[argsWithMixins.length-1] = "mixins.testextension.json";
|
||||
Bootstrap.bootstrap("fr.themode.demo.MainDemo", argsWithMixins);
|
||||
Bootstrap.bootstrap("demo.MainDemo", argsWithMixins);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import org.spongepowered.asm.mixin.Mixins;
|
||||
public class TestExtensionLauncherNoSetup {
|
||||
|
||||
public static void main(String[] args) {
|
||||
Bootstrap.bootstrap("fr.themode.demo.MainDemo", args);
|
||||
Bootstrap.bootstrap("demo.MainDemo", args);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import org.spongepowered.asm.mixin.injection.ModifyVariable;
|
||||
@Mixin(DynamicChunk.class)
|
||||
public class DynamicChunkMixin {
|
||||
|
||||
@ModifyVariable(method = "setBlock", at = @At("HEAD"), index = 4, require = 1, argsOnly = true, remap = false)
|
||||
@ModifyVariable(method = "UNSAFE_setBlock", at = @At("HEAD"), index = 4, require = 1, argsOnly = true, remap = false)
|
||||
public short oopsAllTnt(short blockStateId) {
|
||||
if(blockStateId != 0)
|
||||
return Block.TNT.getBlockId();
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"entrypoint": "testextension.TestExtension",
|
||||
"name": "Test extension",
|
||||
"name": "Test_extension",
|
||||
"codeModifiers": [
|
||||
"testextension.TestModifier"
|
||||
],
|
||||
|
Loading…
Reference in New Issue
Block a user