9
0
mirror of https://github.com/Xiao-MoMi/craft-engine.git synced 2025-12-27 02:49:15 +00:00

重构区块缓存

This commit is contained in:
XiaoMoMi
2025-05-07 19:49:21 +08:00
parent 1b93704a4e
commit c3bf97a3f5
17 changed files with 154 additions and 147 deletions

View File

@@ -42,7 +42,7 @@ public class LegacySlimeWorldDataStorage implements WorldDataStorage {
}
@Override
public void writeChunkAt(@NotNull ChunkPos pos, @NotNull CEChunk chunk, boolean immediately) {
public void writeChunkAt(@NotNull ChunkPos pos, @NotNull CEChunk chunk) {
SlimeChunk slimeChunk = getWorld().getChunk(pos.x, pos.z);
if (slimeChunk == null) return;
CompoundTag nbt = DefaultChunkSerializer.serialize(chunk);

View File

@@ -44,7 +44,7 @@ public class SlimeWorldDataStorage implements WorldDataStorage {
@SuppressWarnings("unchecked")
@Override
public void writeChunkAt(@NotNull ChunkPos pos, @NotNull CEChunk chunk, boolean immediately) {
public void writeChunkAt(@NotNull ChunkPos pos, @NotNull CEChunk chunk) {
SlimeChunk slimeChunk = getWorld().getChunk(pos.x, pos.z);
if (slimeChunk == null) return;
CompoundTag nbt = DefaultChunkSerializer.serialize(chunk);

View File

@@ -196,7 +196,7 @@ public class FastAsyncWorldEditDelegate extends AbstractDelegateExtent {
try {
for (CEChunk ceChunk : this.chunksToSave) {
CraftEngine.instance().debug(() -> "Saving chunk " + ceChunk.chunkPos());
this.ceWorld.worldDataStorage().writeChunkAt(ceChunk.chunkPos(), ceChunk, true);
this.ceWorld.worldDataStorage().writeChunkAt(ceChunk.chunkPos(), ceChunk);
}
this.chunksToSave.clear();
} catch (IOException e) {

View File

@@ -345,8 +345,9 @@ light-system:
force-update-light: false
chunk-system:
# Unloaded chunks may be loaded soon. Delaying serialization can improve performance, especially for those double-dimension mob farms.
delay-serialization: 20 # seconds -1 = disable
# With cache system, those frequently load/unload chunks would consume fewer resources on serialization
# Enabling this option will increase memory consumption to a certain extent
cache-system: true
# 1 = NONE | Compression Speed | Decompress Speed | Compression Ratio | Memory Usage |
# 2 = DEFLATE | Medium-Slow Medium Moderate Low |
# 3 = GZIP | Medium-Slow Medium Moderate Low |

View File

@@ -17,6 +17,7 @@ import net.bytebuddy.implementation.bind.annotation.AllArguments;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.implementation.bind.annotation.This;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
import net.bytebuddy.implementation.bytecode.assign.TypeCasting;
import net.bytebuddy.implementation.bytecode.member.FieldAccess;
import net.bytebuddy.implementation.bytecode.member.MethodReturn;
@@ -77,6 +78,7 @@ public class BukkitInjector {
private static Class<?> clazz$InjectedPalettedContainer;
private static Class<?> clazz$InjectedLevelChunkSection;
private static MethodHandle constructor$InjectedLevelChunkSection;
private static VarHandle varHandle$InjectedPalettedContainer$target;
@@ -104,6 +106,7 @@ public class BukkitInjector {
.name("net.minecraft.world.level.chunk.InjectedPalettedContainer")
.implement(InjectedHolder.Palette.class)
.defineField("target", Reflections.clazz$PalettedContainer, Visibility.PUBLIC)
.defineField("active", boolean.class, Visibility.PUBLIC)
.defineField("cesection", CESection.class, Visibility.PRIVATE)
.defineField("cechunk", CEChunk.class, Visibility.PRIVATE)
.defineField("cepos", SectionPos.class, Visibility.PRIVATE)
@@ -116,6 +119,10 @@ public class BukkitInjector {
.intercept(MethodDelegation.to(GetAndSetInterceptor.INSTANCE))
.method(ElementMatchers.named("target"))
.intercept(FieldAccessor.ofField("target"))
.method(ElementMatchers.named("setTarget"))
.intercept(FieldAccessor.ofField("target").withAssigner(Assigner.DEFAULT, Assigner.Typing.DYNAMIC))
.method(ElementMatchers.named("isActive").or(ElementMatchers.named("setActive")))
.intercept(FieldAccessor.ofField("active"))
.method(ElementMatchers.named("ceSection"))
.intercept(FieldAccessor.ofField("cesection"))
.method(ElementMatchers.named("ceChunk"))
@@ -125,13 +132,14 @@ public class BukkitInjector {
.make()
.load(BukkitInjector.class.getClassLoader())
.getLoaded();
varHandle$InjectedPalettedContainer$target = Objects.requireNonNull(ReflectionUtils.findVarHandle(clazz$InjectedPalettedContainer, "target", Reflections.clazz$PalettedContainer));
//varHandle$InjectedPalettedContainer$target = Objects.requireNonNull(ReflectionUtils.findVarHandle(clazz$InjectedPalettedContainer, "target", Reflections.clazz$PalettedContainer));
// Level Chunk Section
clazz$InjectedLevelChunkSection = byteBuddy
.subclass(Reflections.clazz$LevelChunkSection)
.subclass(Reflections.clazz$LevelChunkSection, ConstructorStrategy.Default.IMITATE_SUPER_CLASS_OPENING)
.name("net.minecraft.world.level.chunk.InjectedLevelChunkSection")
.implement(InjectedHolder.Section.class)
.defineField("active", boolean.class, Visibility.PUBLIC)
.defineField("cesection", CESection.class, Visibility.PRIVATE)
.defineField("cechunk", CEChunk.class, Visibility.PRIVATE)
.defineField("cepos", SectionPos.class, Visibility.PRIVATE)
@@ -143,10 +151,16 @@ public class BukkitInjector {
.intercept(FieldAccessor.ofField("cechunk"))
.method(ElementMatchers.named("cePos"))
.intercept(FieldAccessor.ofField("cepos"))
.method(ElementMatchers.named("isActive").or(ElementMatchers.named("setActive")))
.intercept(FieldAccessor.ofField("active"))
.make()
.load(BukkitInjector.class.getClassLoader())
.getLoaded();
constructor$InjectedLevelChunkSection = MethodHandles.publicLookup().in(clazz$InjectedLevelChunkSection)
.findConstructor(clazz$InjectedLevelChunkSection, MethodType.methodType(void.class, Reflections.clazz$PalettedContainer, Reflections.clazz$PalettedContainer))
.asType(MethodType.methodType(Reflections.clazz$LevelChunkSection, Reflections.clazz$PalettedContainer, Reflections.clazz$PalettedContainer));
// State Predicate
DynamicType.Unloaded<?> alwaysTrue = byteBuddy
.subclass(Reflections.clazz$StatePredicate)
@@ -408,32 +422,50 @@ public class BukkitInjector {
try {
if (Config.injectionTarget()) {
Object container = FastNMS.INSTANCE.field$LevelChunkSection$states(targetSection);
if (!(container instanceof InjectedHolder.Palette)) {
if (!(container instanceof InjectedHolder.Palette holder)) {
InjectedHolder.Palette injectedObject;
if (Config.fastInjection()) {
injectedObject = FastNMS.INSTANCE.createInjectedPalettedContainerHolder(container);
} else {
injectedObject = (InjectedHolder.Palette) Reflections.UNSAFE.allocateInstance(clazz$InjectedPalettedContainer);
varHandle$InjectedPalettedContainer$target.set(injectedObject, container);
injectedObject.setTarget(container);
//varHandle$InjectedPalettedContainer$target.set(injectedObject, container);
}
injectedObject.ceChunk(chunk);
injectedObject.ceSection(ceSection);
injectedObject.cePos(pos);
injectedObject.setActive(true);
Reflections.varHandle$PalettedContainer$data.setVolatile(injectedObject, Reflections.varHandle$PalettedContainer$data.get(container));
Reflections.field$LevelChunkSection$states.set(targetSection, injectedObject);
} else {
holder.ceChunk(chunk);
holder.ceSection(ceSection);
holder.cePos(pos);
holder.setActive(true);
}
} else {
InjectedHolder.Section injectedObject;
if (true) {
injectedObject = FastNMS.INSTANCE.createInjectedLevelChunkSectionHolder(targetSection);
if (!(targetSection instanceof InjectedHolder.Section holder)) {
InjectedHolder.Section injectedObject;
if (Config.fastInjection()) {
injectedObject = FastNMS.INSTANCE.createInjectedLevelChunkSectionHolder(targetSection);
} else {
injectedObject = (InjectedHolder.Section) constructor$InjectedLevelChunkSection.invoke(
FastNMS.INSTANCE.field$LevelChunkSection$states(targetSection), FastNMS.INSTANCE.field$LevelChunkSection$biomes(targetSection));
}
injectedObject.ceChunk(chunk);
injectedObject.ceSection(ceSection);
injectedObject.cePos(pos);
injectedObject.setActive(true);
callback.accept(injectedObject);
} else {
holder.ceChunk(chunk);
holder.ceSection(ceSection);
holder.cePos(pos);
holder.setActive(true);
}
injectedObject.ceChunk(chunk);
injectedObject.ceSection(ceSection);
injectedObject.cePos(pos);
callback.accept(injectedObject);
}
} catch (Exception e) {
CraftEngine.instance().logger().severe("Failed to inject chunk section", e);
} catch (Throwable e) {
CraftEngine.instance().logger().severe("Failed to inject chunk section " + pos, e);
}
}
@@ -450,15 +482,17 @@ public class BukkitInjector {
if (Config.injectionTarget()) {
Object states = FastNMS.INSTANCE.field$LevelChunkSection$states(section);
if (states instanceof InjectedHolder.Palette holder) {
try {
Reflections.field$LevelChunkSection$states.set(section, holder.target());
} catch (ReflectiveOperationException e) {
CraftEngine.instance().logger().severe("Failed to uninject palette", e);
}
holder.setActive(false);
// try {
// Reflections.field$LevelChunkSection$states.set(section, holder.target());
// } catch (ReflectiveOperationException e) {
// CraftEngine.instance().logger().severe("Failed to uninject palette", e);
// }
}
} else {
if (section instanceof InjectedHolder.Section holder) {
return FastNMS.INSTANCE.constructor$LevelChunkSection(holder);
holder.setActive(false);
//return FastNMS.INSTANCE.constructor$LevelChunkSection(holder);
}
}
return section;
@@ -730,7 +764,9 @@ public class BukkitInjector {
int z = (int) args[2];
Object newState = args[3];
Object previousState = superMethod.call();
compareAndUpdateBlockState(x, y, z, newState, previousState, holder);
if (holder.isActive()) {
compareAndUpdateBlockState(x, y, z, newState, previousState, holder);
}
return previousState;
}
}
@@ -747,7 +783,9 @@ public class BukkitInjector {
int z = (int) args[2];
Object newState = args[3];
Object previousState = FastNMS.INSTANCE.method$PalettedContainer$getAndSet(targetStates, x, y, z, newState);
compareAndUpdateBlockState(x, y, z, newState, previousState, holder);
if (holder.isActive()) {
compareAndUpdateBlockState(x, y, z, newState, previousState, holder);
}
return previousState;
}
}

View File

@@ -266,27 +266,24 @@ public class BukkitWorldManager implements WorldManager, Listener {
if (ceChunk != null) {
if (ceChunk.dirty()) {
try {
world.worldDataStorage().writeChunkAt(pos, ceChunk, false);
this.plugin.debug(() -> "[Dirty Chunk]" + pos + " unloaded");
world.worldDataStorage().writeChunkAt(pos, ceChunk);
ceChunk.setDirty(false);
} catch (IOException e) {
this.plugin.logger().warn("Failed to write chunk tag at " + chunk.getX() + " " + chunk.getZ(), e);
}
}
if (Config.restoreVanillaBlocks()) {
boolean unsaved = false;
CESection[] ceSections = ceChunk.sections();
Object worldServer = FastNMS.INSTANCE.field$CraftChunk$worldServer(chunk);
Object chunkSource = FastNMS.INSTANCE.method$ServerLevel$getChunkSource(worldServer);
Object levelChunk = FastNMS.INSTANCE.method$ServerChunkCache$getChunkAtIfLoadedMainThread(chunkSource, chunk.getX(), chunk.getZ());
Object[] sections = FastNMS.INSTANCE.method$ChunkAccess$getSections(levelChunk);
for (int i = 0; i < ceSections.length; i++) {
CESection ceSection = ceSections[i];
Object section = sections[i];
Object uninjectedSection = BukkitInjector.uninjectLevelChunkSection(section);
if (uninjectedSection != section) {
sections[i] = uninjectedSection;
section = uninjectedSection;
}
boolean unsaved = false;
CESection[] ceSections = ceChunk.sections();
Object worldServer = FastNMS.INSTANCE.field$CraftChunk$worldServer(chunk);
Object chunkSource = FastNMS.INSTANCE.method$ServerLevel$getChunkSource(worldServer);
Object levelChunk = FastNMS.INSTANCE.method$ServerChunkCache$getChunkAtIfLoadedMainThread(chunkSource, chunk.getX(), chunk.getZ());
Object[] sections = FastNMS.INSTANCE.method$ChunkAccess$getSections(levelChunk);
for (int i = 0; i < ceSections.length; i++) {
CESection ceSection = ceSections[i];
Object section = sections[i];
BukkitInjector.uninjectLevelChunkSection(section);
if (Config.restoreVanillaBlocks()) {
if (!ceSection.statesContainer().isEmpty()) {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
@@ -301,9 +298,9 @@ public class BukkitWorldManager implements WorldManager, Listener {
}
}
}
if (unsaved && !FastNMS.INSTANCE.method$LevelChunk$isUnsaved(levelChunk)) {
FastNMS.INSTANCE.method$LevelChunk$markUnsaved(levelChunk);
}
}
if (unsaved && !FastNMS.INSTANCE.method$LevelChunk$isUnsaved(levelChunk)) {
FastNMS.INSTANCE.method$LevelChunk$markUnsaved(levelChunk);
}
ceChunk.unload();
}

View File

@@ -101,7 +101,7 @@ public class Config {
protected boolean chunk_system$restore_vanilla_blocks_on_chunk_unload;
protected boolean chunk_system$restore_custom_blocks_on_chunk_load;
protected boolean chunk_system$sync_custom_blocks_on_chunk_load;
protected int chunk_system$delay_serialization;
protected boolean chunk_system$cache_system;
protected boolean chunk_system$injection$use_fast_method;
protected boolean chunk_system$injection$target;
@@ -274,7 +274,7 @@ public class Config {
chunk_system$restore_vanilla_blocks_on_chunk_unload = config.getBoolean("chunk-system.restore-vanilla-blocks-on-chunk-unload", true);
chunk_system$restore_custom_blocks_on_chunk_load = config.getBoolean("chunk-system.restore-custom-blocks-on-chunk-load", true);
chunk_system$sync_custom_blocks_on_chunk_load = config.getBoolean("chunk-system.sync-custom-blocks-on-chunk-load", false);
chunk_system$delay_serialization = config.getInt("chunk-system.delay-serialization", 20);
chunk_system$cache_system = config.getBoolean("chunk-system.cache-system", true);
chunk_system$injection$use_fast_method = config.getBoolean("chunk-system.injection.use-fast-method", false);
if (firstTime) {
chunk_system$injection$target = config.getEnum("chunk-system.injection.target", InjectionTarget.class, InjectionTarget.PALETTE) == InjectionTarget.PALETTE;
@@ -699,8 +699,8 @@ public class Config {
return instance.furniture$collision_entity_type;
}
public static int delaySerialization() {
return instance.chunk_system$delay_serialization;
public static boolean enableChunkCache() {
return instance.chunk_system$cache_system;
}
public static boolean fastInjection() {

View File

@@ -49,7 +49,7 @@ public abstract class CEWorld {
for (Map.Entry<Long, CEChunk> entry : this.loadedChunkMap.entrySet()) {
CEChunk chunk = entry.getValue();
if (chunk.dirty()) {
worldDataStorage.writeChunkAt(new ChunkPos(entry.getKey()), chunk, true);
worldDataStorage.writeChunkAt(new ChunkPos(entry.getKey()), chunk);
chunk.setDirty(false);
}
}

View File

@@ -68,4 +68,17 @@ public class ChunkPos {
public static long asLong(int chunkX, int chunkZ) {
return (long) chunkX & 4294967295L | ((long) chunkZ & 4294967295L) << 32;
}
@Override
public final boolean equals(Object o) {
if (!(o instanceof ChunkPos chunkPos)) return false;
return x == chunkPos.x && z == chunkPos.z;
}
@Override
public int hashCode() {
int result = x;
result = 31 * result + z;
return result;
}
}

View File

@@ -4,6 +4,10 @@ import net.momirealms.craftengine.core.world.SectionPos;
public interface InjectedHolder {
boolean isActive();
void setActive(boolean active);
CESection ceSection();
void ceSection(CESection section);
@@ -22,5 +26,7 @@ public interface InjectedHolder {
interface Palette extends InjectedHolder {
Object target();
void setTarget(Object target);
}
}

View File

@@ -0,0 +1,43 @@
package net.momirealms.craftengine.core.world.chunk.storage;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.Scheduler;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.world.CEWorld;
import net.momirealms.craftengine.core.world.ChunkPos;
import net.momirealms.craftengine.core.world.chunk.CEChunk;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
public class CachedDefaultRegionFileStorage extends DefaultRegionFileStorage {
private final Cache<ChunkPos, CEChunk> chunkCache;
public CachedDefaultRegionFileStorage(Path directory) {
super(directory);
this.chunkCache = Caffeine.newBuilder()
.executor(CraftEngine.instance().scheduler().async())
.scheduler(Scheduler.systemScheduler())
.expireAfterAccess(30, TimeUnit.SECONDS)
.build();
}
@Override
public @NotNull CEChunk readChunkAt(@NotNull CEWorld world, @NotNull ChunkPos pos) throws IOException {
CEChunk chunk = this.chunkCache.getIfPresent(pos);
if (chunk != null) {
return chunk;
}
chunk = super.readChunkAt(world, pos);
this.chunkCache.put(pos, chunk);
return chunk;
}
@Override
public synchronized void close() throws IOException {
super.close();
}
}

View File

@@ -147,7 +147,7 @@ public class DefaultRegionFileStorage implements WorldDataStorage {
}
@Override
public void writeChunkAt(@NotNull ChunkPos pos, @NotNull CEChunk chunk, boolean immediately) throws IOException {
public void writeChunkAt(@NotNull ChunkPos pos, @NotNull CEChunk chunk) throws IOException {
CompoundTag nbt = DefaultChunkSerializer.serialize(chunk);
writeChunkTagAt(pos, nbt);
}

View File

@@ -9,8 +9,8 @@ public class DefaultStorageAdaptor implements StorageAdaptor {
@Override
public @NotNull WorldDataStorage adapt(@NotNull World world) {
if (Config.delaySerialization() > 0) {
return new DelayedDefaultRegionFileStorage(world.directory().resolve(CEWorld.REGION_DIRECTORY), Config.delaySerialization());
if (Config.enableChunkCache()) {
return new CachedDefaultRegionFileStorage(world.directory().resolve(CEWorld.REGION_DIRECTORY));
} else {
return new DefaultRegionFileStorage(world.directory().resolve(CEWorld.REGION_DIRECTORY));
}

View File

@@ -1,85 +0,0 @@
package net.momirealms.craftengine.core.world.chunk.storage;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import net.momirealms.craftengine.core.plugin.CraftEngine;
import net.momirealms.craftengine.core.world.CEWorld;
import net.momirealms.craftengine.core.world.ChunkPos;
import net.momirealms.craftengine.core.world.chunk.CEChunk;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
public class DelayedDefaultRegionFileStorage extends DefaultRegionFileStorage {
private final Cache<ChunkPos, CEChunk> chunkCache;
private boolean isClosed;
public DelayedDefaultRegionFileStorage(Path directory, int time) {
super(directory);
this.chunkCache = Caffeine.newBuilder()
.expireAfterWrite(time, TimeUnit.SECONDS)
.removalListener((ChunkPos key, CEChunk value, RemovalCause cause) -> {
if (key == null || value == null || isClosed) {
return;
}
if (cause == RemovalCause.EXPIRED || cause == RemovalCause.SIZE) {
try {
super.writeChunkAt(key, value, true);
} catch (ClosedChannelException e) {
if (this.isClosed) {
return;
}
CraftEngine.instance().logger().warn("Failed to write chunk at " + key, e);
} catch (IOException e) {
CraftEngine.instance().logger().warn("Failed to write chunk at " + key, e);
}
}
})
.build();
}
@Override
public @NotNull CEChunk readChunkAt(@NotNull CEWorld world, @NotNull ChunkPos pos) throws IOException {
CEChunk chunk = this.chunkCache.asMap().remove(pos);
if (chunk != null) {
return chunk;
}
return super.readChunkAt(world, pos);
}
@Override
public void writeChunkAt(@NotNull ChunkPos pos, @NotNull CEChunk chunk, boolean immediately) throws IOException {
if (immediately) {
super.writeChunkAt(pos, chunk, true);
return;
}
if (chunk.isEmpty()) {
super.writeChunkTagAt(pos, null);
return;
}
this.chunkCache.put(pos, chunk);
}
@Override
public synchronized void close() throws IOException {
this.isClosed = true;
this.saveCache();
this.chunkCache.cleanUp();
super.close();
}
private void saveCache() {
try {
for (var chunk : this.chunkCache.asMap().entrySet()) {
super.writeChunkAt(chunk.getKey(), chunk.getValue(), true);
}
} catch (IOException e) {
CraftEngine.instance().logger().warn("Failed to save chunks", e);
}
this.chunkCache.invalidateAll();
}
}

View File

@@ -309,9 +309,6 @@ public class RegionFile implements AutoCloseable {
}
public void clear(ChunkPos pos) throws IOException {
if (!this.fileChannel.isOpen()) {
throw new ClosedChannelException();
}
int chunkLocation = RegionFile.getChunkLocation(pos);
int sectorInfo = this.sectorInfo.get(chunkLocation);
if (sectorInfo != INFO_NOT_PRESENT) {
@@ -325,9 +322,6 @@ public class RegionFile implements AutoCloseable {
@SuppressWarnings("ResultOfMethodCallIgnored")
protected synchronized void write(ChunkPos pos, ByteBuffer buf) throws IOException {
if (!this.fileChannel.isOpen()) {
throw new ClosedChannelException();
}
// get old offset info
int offsetIndex = RegionFile.getChunkLocation(pos);
int previousSectorInfo = this.sectorInfo.get(offsetIndex);

View File

@@ -12,7 +12,7 @@ public interface WorldDataStorage {
@NotNull
CEChunk readChunkAt(@NotNull CEWorld world, @NotNull ChunkPos pos) throws IOException;
void writeChunkAt(@NotNull ChunkPos pos, @NotNull CEChunk chunk, boolean immediately) throws IOException;
void writeChunkAt(@NotNull ChunkPos pos, @NotNull CEChunk chunk) throws IOException;
void flush() throws IOException;

View File

@@ -2,7 +2,7 @@ org.gradle.jvmargs=-Xmx1G
# Project settings
# Rule: [major update].[feature update].[bug fix]
project_version=0.0.53-beta.6
project_version=0.0.53-beta.7
config_version=32
lang_version=12
project_group=net.momirealms
@@ -50,7 +50,7 @@ byte_buddy_version=1.17.5
ahocorasick_version=0.6.3
snake_yaml_version=2.4
anti_grief_version=0.15
nms_helper_version=0.65.12
nms_helper_version=0.65.15
evalex_version=3.5.0
reactive_streams_version=1.0.4
amazon_awssdk_version=2.31.23