Updated Upstream (CraftBukkit/Spigot) (#7580)

Upstream has released updates that appear to apply and compile correctly.
This update has not been tested by PaperMC and as with ANY update, please do your own testing

Bukkit Changes:
881e06e5 PR-725: Add Item Unlimited Lifetime APIs

CraftBukkit Changes:
74c08312 SPIGOT-6962: Call EntityChangeBlockEvent when when FallingBlockEntity starts to fall
64db5126 SPIGOT-6959: Make /loot command ignore empty items for spawn
2d760831 Increase outdated build delay
9ed7e4fb SPIGOT-6138, SPIGOT-6415: Don't call CreatureSpawnEvent after cross-dimensional travel
fc4ad813 SPIGOT-6895: Trees grown with applyBoneMeal() don't fire the StructureGrowthEvent
59733a2e SPIGOT-6961: Actually return a copy of the ItemMeta

Spigot Changes:
ffceeae3 SPIGOT-6956: Drop unload queue patch as attempt at fixing stop issue
e19ddabd PR-1011: Add Item Unlimited Lifetime APIs
34d40b0e SPIGOT-2942: give command fires PlayerDropItemEvent, cancelling it causes Item Duplication
This commit is contained in:
Nassim Jahnke 2022-03-13 08:47:54 +01:00
parent 1fe6f0bff7
commit 548f257f50
23 changed files with 47 additions and 146 deletions

View file

@ -22,14 +22,14 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
this.updatingChunks.queueUpdate(pos, holder); // Paper - Don't copy
this.modified = true;
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || !this.updatingChunks.getUpdatingValuesCopy().isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper
}
- private static final double UNLOAD_QUEUE_RESIZE_FACTOR = 0.90; // Spigot // Paper - unload more
+ public static final double UNLOAD_QUEUE_RESIZE_FACTOR = 0.90; // Spigot // Paper - unload more
private void processUnloads(BooleanSupplier shouldKeepTicking) {
LongIterator longiterator = this.toDrop.iterator();
- for (int i = 0; longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000); longiterator.remove()) {
+ for (int i = 0; longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000); longiterator.remove()) { // Paper - diff on change
long j = longiterator.nextLong();
ChunkHolder playerchunk = this.updatingChunks.queueRemove(j); // Paper - Don't copy
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
this.regionManagers.get(index).removeChunk(holder.pos.x, holder.pos.z);
}
@ -169,9 +169,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ net.minecraft.server.level.ServerChunkCache chunkProvider = this.world.getChunkSource();
+ net.minecraft.server.level.ChunkMap playerChunkMap = chunkProvider.chunkMap;
+ // copied target determination from PlayerChunkMap
+ int target = Math.min(this.queuedUnloads.size() - 100, (int) (this.queuedUnloads.size() * net.minecraft.server.level.ChunkMap.UNLOAD_QUEUE_RESIZE_FACTOR)); // Paper - Make more aggressive
+ for (java.util.Iterator<QueuedUnload> iterator = this.queuedUnloads.iterator();
+ iterator.hasNext() && (this.queuedUnloads.size() > target || canSleepForTick.getAsBoolean());) {
+
+ java.util.Iterator<QueuedUnload> iterator = this.queuedUnloads.iterator();
+ for (int i = 0; iterator.hasNext() && (i < 200 || this.queuedUnloads.size() > 2000 || canSleepForTick.getAsBoolean()); i++) {
+ QueuedUnload unload = iterator.next();
+ if (unload.unloadTick > currentTick) {
+ break;

View file

@ -57,8 +57,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
@@ -0,0 +0,0 @@ public class CraftMetaArmorStand extends CraftMetaItem {
if (tag.contains(ENTITY_TAG.NBT)) {
this.entityTag = tag.getCompound(ENTITY_TAG.NBT);
+
this.entityTag = tag.getCompound(ENTITY_TAG.NBT).copy();
+ // Paper start
+ if (entityTag.contains(INVISIBLE.NBT)) {
+ invisible = entityTag.getBoolean(INVISIBLE.NBT);

View file

@ -2381,30 +2381,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
}
gameprofilerfiller.pop();
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || !this.updatingChunkMap.isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets();
}
- private static final double UNLOAD_QUEUE_RESIZE_FACTOR = 0.96; // Spigot
+ private static final double UNLOAD_QUEUE_RESIZE_FACTOR = 0.90; // Spigot // Paper - unload more
private void processUnloads(BooleanSupplier shouldKeepTicking) {
LongIterator longiterator = this.toDrop.iterator();
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
if (playerchunk != null) {
this.pendingUnloads.put(j, playerchunk);
this.modified = true;
+ this.scheduleUnload(j, playerchunk); // Paper - Move up - don't leak chunks
// Spigot start
if (!shouldKeepTicking.getAsBoolean() && this.toDrop.size() <= targetSize && activityAccountant.activityTimeIsExhausted()) {
break;
}
// Spigot end
- this.scheduleUnload(j, playerchunk);
+ //this.scheduleUnload(j, playerchunk); // Paper - move up because spigot did a dumb
}
}
activityAccountant.endActivity(); // Spigot
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
((LevelChunk) ichunkaccess).setLoaded(false);
}

View file

@ -50,7 +50,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ Date buildDate = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").parse(Main.class.getPackage().getImplementationVendor()); // Paper
Calendar deadline = Calendar.getInstance();
deadline.add(Calendar.DAY_OF_YEAR, -3);
deadline.add(Calendar.DAY_OF_YEAR, -21);
diff --git a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java b/src/main/java/org/bukkit/craftbukkit/util/Versioning.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/Versioning.java

View file

@ -128,11 +128,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ return this.lightEngine.hasLightWork() || !this.pendingUnloads.isEmpty() || !this.updatingChunks.getUpdatingValuesCopy().isEmpty() || this.poiManager.hasWork() || !this.toDrop.isEmpty() || !this.unloadQueue.isEmpty() || this.queueSorter.hasWork() || this.distanceManager.hasTickets(); // Paper
}
private static final double UNLOAD_QUEUE_RESIZE_FACTOR = 0.90; // Spigot // Paper - unload more
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
while (longiterator.hasNext()) { // Spigot
private void processUnloads(BooleanSupplier shouldKeepTicking) {
LongIterator longiterator = this.toDrop.iterator();
for (int i = 0; longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000); longiterator.remove()) {
long j = longiterator.nextLong();
longiterator.remove(); // Spigot
- ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.remove(j);
+ ChunkHolder playerchunk = this.updatingChunks.queueRemove(j); // Paper - Don't copy

View file

@ -19,10 +19,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ return true;
+ }
+ // Paper end
+
if (!CraftEventFactory.doEntityAddEventCalling(this, entity, spawnReason)) {
// SPIGOT-6415: Don't call spawn event when reason is null. For example when an entity teleports to a new world.
if (spawnReason != null && !CraftEventFactory.doEntityAddEventCalling(this, entity, spawnReason)) {
return false;
}
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java

View file

@ -26,10 +26,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
double d0 = this.getEyeY() - 0.30000001192092896D;
+ // Paper start
+ ItemStack tmp = stack.copy();
+ stack.setCount(0);
+ stack = tmp;
+ ItemStack tmp = itemstack.copy();
+ itemstack.setCount(0);
+ itemstack = tmp;
+ // Paper end
ItemEntity entityitem = new ItemEntity(this.level, this.getX(), d0, this.getZ(), stack);
ItemEntity entityitem = new ItemEntity(this.level, this.getX(), d0, this.getZ(), itemstack);
entityitem.setPickUpDelay(40);

View file

@ -1,70 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Alphaesia <creepashadowz@gmail.com>
Date: Fri, 23 Apr 2021 09:57:56 +1200
Subject: [PATCH] Fix duplicating /give items on item drop cancel
Fixes SPIGOT-2942 (Give command fires PlayerDropItemEvent, cancelling it causes item duplication).
For every stack of items to give, /give puts the item stack straight
into the player's inventory. However, it also summons a "fake item"
at the player's location. When the PlayerDropItemEvent for this fake
item is cancelled, the server attempts to put the item back into the
player's inventory. The result is that the fake item, which is never
meant to be obtained, is combined with the real items injected directly
into the player's inventory. This means more items than the amount
specified in /give are given to the player - one for every stack of
items given. (e.g. /give @s dirt 1 gives you 2 dirt).
While this isn't a big issue for general building usage, it can affect
e.g. adventure maps where the number of items the player receives is
important (and you want to restrict the player from throwing items).
If there are any overflow items that didn't make it into the inventory
(insufficient space), those items are dropped as a real item instead
of a fake one. While cancelling this drop would also result in the
server attempting to put those items into the inventory, since it is
full this has no effect.
Just ignoring cancellation of the PlayerDropItemEvent seems like the
cleanest and least intrusive way to fix it.
diff --git a/src/main/java/net/minecraft/server/commands/GiveCommand.java b/src/main/java/net/minecraft/server/commands/GiveCommand.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/commands/GiveCommand.java
+++ b/src/main/java/net/minecraft/server/commands/GiveCommand.java
@@ -0,0 +0,0 @@ public class GiveCommand {
boolean bl = serverPlayer.getInventory().add(itemStack);
if (bl && itemStack.isEmpty()) {
itemStack.setCount(1);
- ItemEntity itemEntity2 = serverPlayer.drop(itemStack, false);
+ ItemEntity itemEntity2 = serverPlayer.drop(itemStack, false, false, true); // Paper - Fix duplicating /give items on item drop cancel
if (itemEntity2 != null) {
itemEntity2.makeFakeItem();
}
diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/world/entity/player/Player.java
+++ b/src/main/java/net/minecraft/world/entity/player/Player.java
@@ -0,0 +0,0 @@ public abstract class Player extends LivingEntity {
@Nullable
public ItemEntity drop(ItemStack stack, boolean throwRandomly, boolean retainOwnership) {
+ // Paper start - Fix duplicating /give items on item drop cancel
+ return this.drop(stack, throwRandomly, retainOwnership, false);
+ }
+
+ @Nullable
+ public ItemEntity drop(ItemStack stack, boolean throwRandomly, boolean retainOwnership, boolean alwaysSucceed) {
+ // Paper end
if (stack.isEmpty()) {
return null;
} else {
@@ -0,0 +0,0 @@ public abstract class Player extends LivingEntity {
PlayerDropItemEvent event = new PlayerDropItemEvent(player, drop);
this.level.getCraftServer().getPluginManager().callEvent(event);
- if (event.isCancelled()) {
+ if (event.isCancelled() && !alwaysSucceed) { // Paper - Fix duplicating /give items on item drop cancel
org.bukkit.inventory.ItemStack cur = player.getInventory().getItemInHand();
if (retainOwnership && (cur == null || cur.getAmount() == 0)) {
// The complete stack was dropped

View file

@ -9,11 +9,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
--- a/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java
+++ b/src/main/java/org/bukkit/craftbukkit/block/CraftBlock.java
@@ -0,0 +0,0 @@ public class CraftBlock implements Block {
Direction direction = CraftBlock.blockFaceToNotch(face);
UseOnContext context = new UseOnContext(this.getCraftWorld().getHandle(), null, InteractionHand.MAIN_HAND, Items.BONE_MEAL.getDefaultInstance(), new BlockHitResult(Vec3.ZERO, direction, this.getPosition(), false));
}
}
- return BoneMealItem.applyBonemeal(context) == InteractionResult.SUCCESS;
+ return BoneMealItem.applyBonemeal(context) == InteractionResult.CONSUME; // Paper - CONSUME is returned on success server-side (see BoneMealItem.applyBoneMeal and InteractionResult.sidedSuccess(boolean))
- return result == InteractionResult.SUCCESS && (event == null || !event.isCancelled());
+ return result == InteractionResult.CONSUME && (event == null || !event.isCancelled()); // Paper - CONSUME is returned on success server-side (see BoneMealItem.applyBoneMeal and InteractionResult.sidedSuccess(boolean))
}
@Override

View file

@ -36,7 +36,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+++ b/src/main/java/org/bukkit/craftbukkit/Main.java
@@ -0,0 +0,0 @@ public class Main {
Calendar deadline = Calendar.getInstance();
deadline.add(Calendar.DAY_OF_YEAR, -3);
deadline.add(Calendar.DAY_OF_YEAR, -21);
if (buildDate.before(deadline.getTime())) {
- System.err.println("*** Error, this build is outdated ***");
+ // Paper start - This is some stupid bullshit

View file

@ -31,8 +31,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
- private static final int SAMPLE_INTERVAL = 100;
+ private static final int SAMPLE_INTERVAL = 20; // Paper
public final double[] recentTps = new double[ 3 ];
public final SlackActivityAccountant slackActivityAccountant = new SlackActivityAccountant();
// Spigot end
public static long currentTickLong = 0L; // Paper
@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
{
return ( avg * exp ) + ( tps * ( 1 - exp ) );

View file

@ -60,7 +60,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
this.persistentDataContainer.putAll(meta.persistentDataContainer.getRaw());
@@ -0,0 +0,0 @@ class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDataMeta {
this.persistentDataContainer.put(key, compound.get(key));
this.persistentDataContainer.put(key, compound.get(key).copy());
}
}
+ // Paper start - Implement an API for CanPlaceOn and CanDestroy NBT values

View file

@ -49,10 +49,10 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
}
// CraftBukkit end
+ // Paper start - remove player from map on drop
+ if (stack.getItem() == Items.FILLED_MAP) {
+ MapItemSavedData worldmap = MapItem.getSavedData(stack, this.level);
+ if (itemstack.getItem() == Items.FILLED_MAP) {
+ MapItemSavedData worldmap = MapItem.getSavedData(itemstack, this.level);
+ if (worldmap != null) {
+ worldmap.tickCarriedBy(this, stack);
+ worldmap.tickCarriedBy(this, itemstack);
+ }
+ }
+ // Paper end

View file

@ -37,7 +37,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java
+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftItem.java
@@ -0,0 +0,0 @@ public class CraftItem extends CraftEntity implements Item {
item.age = value;
}
}
+ // Paper Start

View file

@ -5005,8 +5005,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
private static final int SAMPLE_INTERVAL = 100;
public final double[] recentTps = new double[ 3 ];
public final SlackActivityAccountant slackActivityAccountant = new SlackActivityAccountant();
// Spigot end
+ public static long currentTickLong = 0L; // Paper

View file

@ -19,20 +19,19 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
this.storageName = path.getFileName().toString();
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
// Spigot start
org.spigotmc.SlackActivityAccountant activityAccountant = this.level.getServer().slackActivityAccountant;
activityAccountant.startActivity(0.5);
- int targetSize = (int) (this.toDrop.size() * ChunkMap.UNLOAD_QUEUE_RESIZE_FACTOR);
+ int targetSize = Math.min(this.toDrop.size() - 100, (int) (this.toDrop.size() * ChunkMap.UNLOAD_QUEUE_RESIZE_FACTOR)); // Paper - Make more aggressive
// Spigot end
while (longiterator.hasNext()) { // Spigot
private void processUnloads(BooleanSupplier shouldKeepTicking) {
LongIterator longiterator = this.toDrop.iterator();
-
for (int i = 0; longiterator.hasNext() && (shouldKeepTicking.getAsBoolean() || i < 200 || this.toDrop.size() > 2000); longiterator.remove()) {
long j = longiterator.nextLong();
ChunkHolder playerchunk = (ChunkHolder) this.updatingChunkMap.remove(j);
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
}
}
activityAccountant.endActivity(); // Spigot
- int k = Math.max(0, this.unloadQueue.size() - 2000);
+ int k = Math.max(0, Math.min(100, this.unloadQueue.size() - (int) (this.unloadQueue.size() * UNLOAD_QUEUE_RESIZE_FACTOR))); // Paper - Target this queue as well
+ int k = Math.max(100, this.unloadQueue.size() - 2000); // Paper - Unload more than just up to queue size 2000
Runnable runnable;

View file

@ -142,7 +142,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
+ playerchunk.onChunkRemove(); // Paper
this.pendingUnloads.put(j, playerchunk);
this.modified = true;
this.scheduleUnload(j, playerchunk); // Paper - Move up - don't leak chunks
++i;
@@ -0,0 +0,0 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
return this.anyPlayerCloseEnoughForSpawning(pos, false);
}

View file

@ -49,7 +49,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
--- a/src/main/java/org/bukkit/craftbukkit/Main.java
+++ b/src/main/java/org/bukkit/craftbukkit/Main.java
@@ -0,0 +0,0 @@ public class Main {
deadline.add(Calendar.DAY_OF_YEAR, -3);
deadline.add(Calendar.DAY_OF_YEAR, -21);
if (buildDate.before(deadline.getTime())) {
System.err.println("*** Error, this build is outdated ***");
- System.err.println("*** Please download a new build as per instructions from https://www.spigotmc.org/go/outdated-spigot ***");

View file

@ -775,9 +775,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
-import org.bukkit.craftbukkit.SpigotTimings; // Spigot
+import co.aikar.timings.MinecraftTimings; // Paper
import org.spigotmc.SlackActivityAccountant; // Spigot
public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements CommandSource, AutoCloseable {
@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
// CraftBukkit end
@ -826,7 +826,6 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
public void tickServer(BooleanSupplier shouldKeepTicking) {
- SpigotTimings.serverTickTimer.startTiming(); // Spigot
+ co.aikar.timings.TimingsManager.FULL_SERVER_TICK.startTiming(); // Paper
this.slackActivityAccountant.tickStarted(); // Spigot
long i = Util.getNanos();
+ // Paper start - move oversleep into full server tick
@ -862,9 +861,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
long l = this.tickTimes[this.tickCount % 100] = Util.getNanos() - i;
@@ -0,0 +0,0 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.frameTimer.logFrameDuration(i1 - i);
this.profiler.pop();
org.spigotmc.WatchdogThread.tick(); // Spigot
this.slackActivityAccountant.tickEnded(l); // Spigot
- SpigotTimings.serverTickTimer.stopTiming(); // Spigot
- org.spigotmc.CustomTimingsHandler.tick(); // Spigot
+ co.aikar.timings.TimingsManager.FULL_SERVER_TICK.stopTiming(); // Paper

View file

@ -9,7 +9,7 @@ diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -0,0 +0,0 @@ import org.spigotmc.SlackActivityAccountant; // Spigot
@@ -0,0 +0,0 @@ import co.aikar.timings.MinecraftTimings; // Paper
public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTask> implements CommandSource, AutoCloseable {

@ -1 +1 @@
Subproject commit e25c6a75523b5122f539a5a59dcf0275c3213a5a
Subproject commit 881e06e5db821ef829b41e372bbcafa1df9670ab

@ -1 +1 @@
Subproject commit 808cb7ca5c135e65e2d23e8ab59ee891b5bc53dc
Subproject commit 9ed7e4fbe4c0cd2076a52d65a9ea8ae810d0e176

@ -1 +1 @@
Subproject commit fb0dd5f518e866748a20ee2c753edc3c6b9392d2
Subproject commit ffceeae314d56fe07395e3e8f8262c0484d2bbd1